Compare commits

...

25 Commits
2.13.0 ... main

Author SHA1 Message Date
net909
e25d5d76e9 支持部署到阿里云全球加速 2026-02-17 14:30:57 +08:00
net909
c0e72908ab 阿里云ESA超过免费配额之后,自动删除最旧的证书 2026-02-17 13:45:09 +08:00
TomyJan
867785b774
fix: cf 优选增加 xingpingcn.top 接口 & perf: cf 优选 ip 数量上限 50 个 (#396)
* fix: cf 优选增加 xingpingcn.top 接口

* perf: cf 优选 ip 数量上限 50 个

* Update app/service/OptimizeService.php

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update app/service/OptimizeService.php

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update app/controller/Optimizeip.php

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: min num limit

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-12 22:23:21 +08:00
dependabot[bot]
d579ca07af
Bump cccyun/php-whois from 1.2 to 1.3 (#398)
Bumps [cccyun/php-whois](https://github.com/netcccyun/php-whois) from 1.2 to 1.3.
- [Release notes](https://github.com/netcccyun/php-whois/releases)
- [Commits](https://github.com/netcccyun/php-whois/compare/1.2...1.3)

---
updated-dependencies:
- dependency-name: cccyun/php-whois
  dependency-version: '1.3'
  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>
2026-02-12 22:20:54 +08:00
net909
7161caf0a5 ssh私钥自动删除 2026-02-02 19:27:31 +08:00
net909
c91b116466 Merge branch 'main' of ssh://ssh.github.com:443/netcccyun/dnsmgr 2026-01-30 23:16:28 +08:00
耗子
b2d27b18a3
feat: 支持AcePanel 3.0部署 (#394) 2026-01-30 23:13:11 +08:00
net909
ee45ddd7ec 新增amh面板部署 2026-01-28 20:53:54 +08:00
TomyJan
9b66b020c9
fix: cf暂停@解析 (#390) 2026-01-27 11:28:56 +08:00
net909
ec16c3fc8b 修复LiteSSL添加DNS失败 2026-01-25 11:12:41 +08:00
net909
d1eaaec650 修复fnOS部署失败 2026-01-24 16:45:24 +08:00
net909
224c27d796 优化青云DNS翻页 2026-01-24 12:27:38 +08:00
net909
6aea445259 新增青云DNS 2026-01-23 23:43:01 +08:00
net909
86c557face 群机器人通知支持@用户 2026-01-23 23:24:02 +08:00
net909
70d3922013 批量添加解析支持设置备注 2026-01-22 21:43:22 +08:00
net909
e56122d7d0 Merge branch 'main' of ssh://ssh.github.com:443/netcccyun/dnsmgr 2026-01-17 22:40:59 +08:00
net909
6694631a9a 域名账户新增支持阿里云ESA、腾讯云EO
优化域名账户新增/编辑页面
2026-01-17 22:40:38 +08:00
net909
2c03dedba0 新增LiteSSL证书类型 2026-01-17 22:39:03 +08:00
net909
095063dcad 已配置好.env的情况下安装不需要配置数据库连接 2026-01-17 22:36:34 +08:00
dependabot[bot]
b6eec27d06
Bump topthink/framework from 8.1.3 to 8.1.4 (#382)
Bumps [topthink/framework](https://github.com/top-think/framework) from 8.1.3 to 8.1.4.
- [Release notes](https://github.com/top-think/framework/releases)
- [Commits](https://github.com/top-think/framework/compare/v8.1.3...v8.1.4)

---
updated-dependencies:
- dependency-name: topthink/framework
  dependency-version: 8.1.4
  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>
2026-01-16 21:36:59 +08:00
net909
b400a62ef9 增加宝塔域名解析管理,修复spaceship解析 2026-01-13 15:54:23 +08:00
dependabot[bot]
36a731d672
Bump phpmailer/phpmailer from 7.0.1 to 7.0.2 (#377)
Bumps [phpmailer/phpmailer](https://github.com/PHPMailer/PHPMailer) from 7.0.1 to 7.0.2.
- [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/v7.0.1...v7.0.2)

---
updated-dependencies:
- dependency-name: phpmailer/phpmailer
  dependency-version: 7.0.2
  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>
2026-01-12 23:57:50 +08:00
dependabot[bot]
dcd829586c
Bump topthink/think-orm from 4.0.50 to 4.0.51 (#365)
Bumps [topthink/think-orm](https://github.com/top-think/think-orm) from 4.0.50 to 4.0.51.
- [Release notes](https://github.com/top-think/think-orm/releases)
- [Commits](https://github.com/top-think/think-orm/compare/v4.0.50...v4.0.51)

---
updated-dependencies:
- dependency-name: topthink/think-orm
  dependency-version: 4.0.51
  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>
2026-01-02 23:06:41 +08:00
net909
fb69ed702b 新增华为云OBS、天翼云函数计算部署,阿里云、腾讯云等新增上传到证书管理选项 2025-12-29 22:29:54 +08:00
深山大柠檬
137193d465
修复1Panel主节点部署失败,支持同时部署主节点和子节点证书,改进日志输出和配置说明 (#364)
* 1panel支持多个子节点部署

* 1Panel子节点/主节点BUG修复,现在可以同时部署子节点和主节点的证书

- 修复主节点部署失败问题
- 支持同时部署主节点和所有指定的子节点
- 改进日志输出和配置说明
2025-12-29 10:43:06 +08:00
77 changed files with 4328 additions and 743 deletions

View File

@ -605,3 +605,10 @@ function getDomainDate($domain)
throw new Exception('查询域名whois失败: ' . $e->getMessage());
}
}
function checkTableExists($table)
{
$prefix = env('database.prefix', 'dnsmgr_');
$res = Db::query("SHOW TABLES LIKE '" . $prefix . $table . "'");
return !empty($res);
}

View File

@ -16,7 +16,6 @@ class Domain extends BaseController
public function account()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
View::assign('dnsconfig', DnsHelper::$dns_config);
return view();
}
@ -29,7 +28,7 @@ class Domain extends BaseController
$select = Db::name('account');
if (!empty($kw)) {
$select->whereLike('ak|remark', '%' . $kw . '%');
$select->whereLike('name|remark', '%' . $kw . '%');
}
$total = $select->count();
$rows = $select->order('id', 'desc')->limit($offset, $limit)->select();
@ -37,39 +36,49 @@ class Domain extends BaseController
$list = [];
foreach ($rows as $row) {
$row['typename'] = DnsHelper::$dns_config[$row['type']]['name'];
$row['icon'] = DnsHelper::$dns_config[$row['type']]['icon'];
$list[] = $row;
}
return json(['total' => $total, 'rows' => $list]);
}
public function account_add()
{
if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]);
$action = input('param.action');
$account = null;
if ($action == 'edit') {
$id = input('get.id/d');
$account = Db::name('account')->where('id', $id)->find();
if (empty($account)) return $this->alert('error', '域名账户不存在');
}
View::assign('info', $account);
View::assign('typeList', DnsHelper::getList());
View::assign('action', $action);
return View::fetch();
}
public function account_op()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$act = input('param.act');
if ($act == 'get') {
$id = input('post.id/d');
$row = Db::name('account')->where('id', $id)->find();
if (!$row) return json(['code' => -1, 'msg' => '域名账户不存在']);
return json(['code' => 0, 'data' => $row]);
} elseif ($act == 'add') {
$action = input('param.action');
if ($action == 'add') {
$type = input('post.type');
$ak = input('post.ak', null, 'trim');
$sk = input('post.sk', null, 'trim');
$ext = input('post.ext', null, 'trim');
$name = input('post.name', null, 'trim');
$config = input('post.config', null, 'trim');
$remark = input('post.remark', null, 'trim');
$proxy = input('post.proxy/d', 0);
if (empty($ak) || empty($sk)) return json(['code' => -1, 'msg' => 'AccessKey和SecretKey不能为空']);
if (Db::name('account')->where('type', $type)->where('ak', $ak)->find()) {
if (empty($name) || empty($config)) return json(['code' => -1, 'msg' => '必填参数不能为空']);
if (Db::name('account')->where('type', $type)->where('name', $name)->find()) {
return json(['code' => -1, 'msg' => '域名账户已存在']);
}
Db::startTrans();
$id = Db::name('account')->insertGetId([
'type' => $type,
'ak' => $ak,
'sk' => $sk,
'ext' => $ext,
'proxy' => $proxy,
'name' => $name,
'config' => $config,
'remark' => $remark,
'addtime' => date('Y-m-d H:i:s'),
]);
@ -86,27 +95,24 @@ class Domain extends BaseController
Db::rollback();
return json(['code' => -1, 'msg' => 'DNS模块(' . $type . ')不存在']);
}
} elseif ($act == 'edit') {
} elseif ($action == 'edit') {
$id = input('post.id/d');
$row = Db::name('account')->where('id', $id)->find();
if (!$row) return json(['code' => -1, 'msg' => '域名账户不存在']);
$type = input('post.type');
$ak = input('post.ak', null, 'trim');
$sk = input('post.sk', null, 'trim');
$ext = input('post.ext', null, 'trim');
$name = input('post.name', null, 'trim');
$config = input('post.config', null, 'trim');
$remark = input('post.remark', null, 'trim');
$proxy = input('post.proxy/d', 0);
if (empty($ak) || empty($sk)) return json(['code' => -1, 'msg' => 'AccessKey和SecretKey不能为空']);
if (Db::name('account')->where('type', $type)->where('ak', $ak)->where('id', '<>', $id)->find()) {
if (empty($name) || empty($config)) return json(['code' => -1, 'msg' => '必填参数不能为空']);
if (Db::name('account')->where('type', $type)->where('name', $name)->where('id', '<>', $id)->find()) {
return json(['code' => -1, 'msg' => '域名账户已存在']);
}
Db::startTrans();
Db::name('account')->where('id', $id)->update([
'type' => $type,
'ak' => $ak,
'sk' => $sk,
'ext' => $ext,
'proxy' => $proxy,
'name' => $name,
'config' => $config,
'remark' => $remark,
'remark' => $remark,
]);
$dns = DnsHelper::getModel($id);
@ -122,7 +128,7 @@ class Domain extends BaseController
Db::rollback();
return json(['code' => -1, 'msg' => 'DNS模块(' . $type . ')不存在']);
}
} elseif ($act == 'del') {
} elseif ($action == 'del') {
$id = input('post.id/d');
$dcount = DB::name('domain')->where('aid', $id)->count();
if ($dcount > 0) return json(['code' => -1, 'msg' => '该域名账户下存在域名,无法删除']);
@ -185,10 +191,17 @@ class Domain extends BaseController
$order = input('post.order', null, 'trim');
$offset = input('post.offset/d', 0);
$limit = input('post.limit/d', 10);
$id = input('post.id');
$aid = input('post.aid', null, 'trim');
$select = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id');
if (!empty($kw)) {
$select->whereLike('name|A.remark', '%' . $kw . '%');
if (!empty($id)) {
$select->where('A.id', $id);
} elseif (!empty($kw)) {
$select->whereLike('A.name|A.remark', '%' . $kw . '%');
}
if (!empty($aid)) {
$select->where('A.aid', $aid);
}
if (!empty($type)) {
$select->whereLike('B.type', $type);
@ -225,6 +238,7 @@ class Domain extends BaseController
$list = [];
foreach ($rows as $row) {
$row['typename'] = DnsHelper::$dns_config[$row['type']]['name'];
$row['icon'] = DnsHelper::$dns_config[$row['type']]['icon'];
$list[] = $row;
}
@ -443,6 +457,9 @@ class Domain extends BaseController
View::assign('recordLine', $recordLineArr);
View::assign('minTTL', $minTTL ? $minTTL : 1);
View::assign('dnsconfig', $dnsconfig);
if ($dnstype == 'qingcloud') {
return view('qingcloud');
}
return view();
}
@ -823,6 +840,7 @@ class Domain extends BaseController
$line = input('post.line', null, 'trim');
$ttl = input('post.ttl/d', 600);
$mx = input('post.mx/d', 1);
$remark = input('post.remark', null, 'trim');
$recordlist = explode("\n", $record);
if (empty($record) || empty($recordlist)) {
@ -844,7 +862,7 @@ class Domain extends BaseController
$arr = explode(' ', $record);
if (empty($record) || empty($arr[0]) || empty($arr[1])) continue;
$thistype = empty($type) ? getDnsType($arr[1]) : $type;
$recordid = $dns->addDomainRecord($arr[0], $thistype, $arr[1], $line, $ttl, $mx);
$recordid = $dns->addDomainRecord($arr[0], $thistype, $arr[1], $line, $ttl, $mx, null, $remark);
if ($recordid) {
$this->add_log($drow['name'], '添加解析', $arr[0].' ['.$thistype.'] '.$arr[1].' (线路:'.$line.' TTL:'.$ttl.')');
$success++;

View File

@ -54,8 +54,6 @@ class Index extends BaseController
if (config('app.dbversion') && config_get('version') != config('app.dbversion')) {
$this->db_update();
config_set('version', config('app.dbversion'));
Cache::clear();
}
$tmp = 'version()';
@ -87,6 +85,27 @@ class Index extends BaseController
} catch (Exception $e) {
}
}
config_set('version', config('app.dbversion'));
Cache::clear();
if(Db::name('account')->count() > 0 && Db::name('account')->whereNotNull('config')->count() == 0) {
$accounts = Db::name('account')->select();
foreach ($accounts as $account) {
if (!empty($account['config']) || !isset(\app\lib\DnsHelper::$dns_config[$account['type']])) continue;
$config = [];
$account_fields = ['name', 'sk', 'ext'];
$i = 0;
foreach(\app\lib\DnsHelper::$dns_config[$account['type']]['config'] as $field => $item) {
if ($field == 'proxy') {
$config[$field] = $account['proxy'];
break;
}
if ($i >= 3) break;
$account_field = $account_fields[$i++];
$config[$field] = isset($account[$account_field]) ? $account[$account_field] : '';
}
Db::name('account')->where('id', $account['id'])->update(['config' => json_encode($config)]);
}
}
}
public function changeskin()

View File

@ -7,82 +7,128 @@ use Exception;
use app\BaseController;
use think\facade\Cache;
use think\facade\Request;
use think\facade\View;
use think\facade\Db;
class Install extends BaseController
{
public function index()
{
$dbconfig = '0';
if (file_exists(app()->getRootPath() . '.env')) {
return '当前已经安装成功,如果需要重新安装,请手动删除根目录.env文件';
if (checkTableExists('config') || checkTableExists('user') || checkTableExists('domain')) {
return '当前已经安装成功,如果需要重新安装,请手动删除根目录.env文件';
} else {
$dbconfig = '1';
}
}
if (Request::isPost()) {
$mysql_host = input('post.mysql_host', null, 'trim');
$mysql_port = intval(input('post.mysql_port', '3306'));
$mysql_user = input('post.mysql_user', null, 'trim');
$mysql_pwd = input('post.mysql_pwd', null, 'trim');
$mysql_name = input('post.mysql_name', null, 'trim');
$mysql_prefix = input('post.mysql_prefix', 'cloud_', 'trim');
$admin_username = input('post.admin_username', null, 'trim');
$admin_password = input('post.admin_password', null, 'trim');
if ($dbconfig == '1') {
$admin_username = input('post.admin_username', null, 'trim');
$admin_password = input('post.admin_password', null, 'trim');
if (!$mysql_host || !$mysql_user || !$mysql_pwd || !$mysql_name || !$admin_username || !$admin_password) {
return json(['code' => 0, 'msg' => '必填项不能为空']);
}
if (!$admin_username || !$admin_password) {
return json(['code' => 0, 'msg' => '必填项不能为空']);
}
$configData = file_get_contents(app()->getRootPath() . '.example.env');
$configData = str_replace(['{dbhost}', '{dbname}', '{dbuser}', '{dbpwd}', '{dbport}', '{dbprefix}'], [$mysql_host, $mysql_name, $mysql_user, $mysql_pwd, $mysql_port, $mysql_prefix], $configData);
$sqls = file_get_contents(app()->getAppPath() . 'sql/install.sql');
$sqls = explode(';', $sqls);
$mysql_prefix = env('database.prefix', 'dnsmgr_');
try {
$DB = new PDO("mysql:host=" . $mysql_host . ";dbname=" . $mysql_name . ";port=" . $mysql_port, $mysql_user, $mysql_pwd);
} catch (Exception $e) {
if ($e->getCode() == 2002) {
$errorMsg = '连接数据库失败:数据库地址填写错误!';
} elseif ($e->getCode() == 1045) {
$errorMsg = '连接数据库失败:数据库用户名或密码填写错误!';
} elseif ($e->getCode() == 1049) {
$errorMsg = '连接数据库失败:数据库名不存在!';
$password = password_hash($admin_password, PASSWORD_DEFAULT);
$sqls[] = "REPLACE INTO `" . $mysql_prefix . "config` VALUES ('sys_key', '" . random(16) . "')";
$sqls[] = "INSERT INTO `" . $mysql_prefix . "user` (`username`,`password`,`level`,`regtime`,`lasttime`,`status`) VALUES ('" . addslashes($admin_username) . "', '$password', 2, NOW(), NOW(), 1)";
$success = 0;
$error = 0;
$errorMsg = null;
foreach ($sqls as $value) {
$value = trim($value);
if (empty($value)) continue;
$value = str_replace('dnsmgr_', $mysql_prefix, $value);
if (Db::execute($value) === false) {
$error++;
$dberror = Db::getErrorInfo();
$errorMsg .= $dberror . "\n";
} else {
$success++;
}
}
if (empty($errorMsg)) {
Cache::clear();
return json(['code' => 1, 'msg' => '安装完成成功执行SQL语句' . $success . '条']);
} else {
$errorMsg = '连接数据库失败:' . $e->getMessage();
return json(['code' => 0, 'msg' => $errorMsg]);
}
return json(['code' => 0, 'msg' => $errorMsg]);
}
$DB->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$DB->exec("set sql_mode = ''");
$DB->exec("set names utf8");
$sqls = file_get_contents(app()->getAppPath() . 'sql/install.sql');
$sqls = explode(';', $sqls);
$password = password_hash($admin_password, PASSWORD_DEFAULT);
$sqls[] = "REPLACE INTO `" . $mysql_prefix . "config` VALUES ('sys_key', '" . random(16) . "')";
$sqls[] = "INSERT INTO `" . $mysql_prefix . "user` (`username`,`password`,`level`,`regtime`,`lasttime`,`status`) VALUES ('" . addslashes($admin_username) . "', '$password', 2, NOW(), NOW(), 1)";
$success = 0;
$error = 0;
$errorMsg = null;
foreach ($sqls as $value) {
$value = trim($value);
if (empty($value)) continue;
$value = str_replace('dnsmgr_', $mysql_prefix, $value);
if ($DB->exec($value) === false) {
$error++;
$dberror = $DB->errorInfo();
$errorMsg .= $dberror[2] . "\n";
} else {
$success++;
}
}
if (empty($errorMsg)) {
if (!file_put_contents(app()->getRootPath() . '.env', $configData)) {
return json(['code' => 0, 'msg' => '保存失败,请确保网站根目录有写入权限']);
}
Cache::clear();
return json(['code' => 1, 'msg' => '安装完成成功执行SQL语句' . $success . '条']);
} else {
return json(['code' => 0, 'msg' => $errorMsg]);
$mysql_host = input('post.mysql_host', null, 'trim');
$mysql_port = intval(input('post.mysql_port', '3306'));
$mysql_user = input('post.mysql_user', null, 'trim');
$mysql_pwd = input('post.mysql_pwd', null, 'trim');
$mysql_name = input('post.mysql_name', null, 'trim');
$mysql_prefix = input('post.mysql_prefix', 'cloud_', 'trim');
$admin_username = input('post.admin_username', null, 'trim');
$admin_password = input('post.admin_password', null, 'trim');
if (!$mysql_host || !$mysql_user || !$mysql_pwd || !$mysql_name || !$admin_username || !$admin_password) {
return json(['code' => 0, 'msg' => '必填项不能为空']);
}
$configData = file_get_contents(app()->getRootPath() . '.example.env');
$configData = str_replace(['{dbhost}', '{dbname}', '{dbuser}', '{dbpwd}', '{dbport}', '{dbprefix}'], [$mysql_host, $mysql_name, $mysql_user, $mysql_pwd, $mysql_port, $mysql_prefix], $configData);
try {
$DB = new PDO("mysql:host=" . $mysql_host . ";dbname=" . $mysql_name . ";port=" . $mysql_port, $mysql_user, $mysql_pwd);
} catch (Exception $e) {
if ($e->getCode() == 2002) {
$errorMsg = '连接数据库失败:数据库地址填写错误!';
} elseif ($e->getCode() == 1045) {
$errorMsg = '连接数据库失败:数据库用户名或密码填写错误!';
} elseif ($e->getCode() == 1049) {
$errorMsg = '连接数据库失败:数据库名不存在!';
} else {
$errorMsg = '连接数据库失败:' . $e->getMessage();
}
return json(['code' => 0, 'msg' => $errorMsg]);
}
$DB->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$DB->exec("set sql_mode = ''");
$DB->exec("set names utf8");
$sqls = file_get_contents(app()->getAppPath() . 'sql/install.sql');
$sqls = explode(';', $sqls);
$password = password_hash($admin_password, PASSWORD_DEFAULT);
$sqls[] = "REPLACE INTO `" . $mysql_prefix . "config` VALUES ('sys_key', '" . random(16) . "')";
$sqls[] = "INSERT INTO `" . $mysql_prefix . "user` (`username`,`password`,`level`,`regtime`,`lasttime`,`status`) VALUES ('" . addslashes($admin_username) . "', '$password', 2, NOW(), NOW(), 1)";
$success = 0;
$error = 0;
$errorMsg = null;
foreach ($sqls as $value) {
$value = trim($value);
if (empty($value)) continue;
$value = str_replace('dnsmgr_', $mysql_prefix, $value);
if ($DB->exec($value) === false) {
$error++;
$dberror = $DB->errorInfo();
$errorMsg .= $dberror[2] . "\n";
} else {
$success++;
}
}
if (empty($errorMsg)) {
if (!file_put_contents(app()->getRootPath() . '.env', $configData)) {
return json(['code' => 0, 'msg' => '保存失败,请确保网站根目录有写入权限']);
}
Cache::clear();
return json(['code' => 1, 'msg' => '安装完成成功执行SQL语句' . $success . '条']);
} else {
return json(['code' => 0, 'msg' => $errorMsg]);
}
}
}
View::assign('dbconfig', $dbconfig);
return view();
}
}

View File

@ -85,8 +85,11 @@ class Optimizeip extends BaseController
if (empty($task['did']) || empty($task['rr']) || empty($task['ip_type']) || empty($task['recordnum']) || empty($task['ttl'])) {
return json(['code' => -1, 'msg' => '必填项不能为空']);
}
if ($task['recordnum'] > 5) {
return json(['code' => -1, 'msg' => '解析数量不能超过5个']);
if ($task['recordnum'] < 1) {
return json(['code' => -1, 'msg' => '解析数量不能少于1个']);
}
if ($task['recordnum'] > 50) {
return json(['code' => -1, 'msg' => '解析数量不能超过50个']);
}
if (Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->find()) {
return json(['code' => -1, 'msg' => '当前域名的优选IP任务已存在']);
@ -109,8 +112,11 @@ class Optimizeip extends BaseController
if (empty($task['did']) || empty($task['rr']) || empty($task['ip_type']) || empty($task['recordnum']) || empty($task['ttl'])) {
return json(['code' => -1, 'msg' => '必填项不能为空']);
}
if ($task['recordnum'] > 5) {
return json(['code' => -1, 'msg' => '解析数量不能超过5个']);
if ($task['recordnum'] < 1) {
return json(['code' => -1, 'msg' => '解析数量不能少于1个']);
}
if ($task['recordnum'] > 50) {
return json(['code' => -1, 'msg' => '解析数量不能超过50个']);
}
if (Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->where('id', '<>', $id)->find()) {
return json(['code' => -1, 'msg' => '当前域名的优选IP任务已存在']);

View File

@ -95,11 +95,12 @@ class System extends BaseController
public function proxytest()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$proxy_server = trim($_POST['proxy_server']);
$proxy_port = $_POST['proxy_port'];
$proxy_user = trim($_POST['proxy_user']);
$proxy_pwd = trim($_POST['proxy_pwd']);
$proxy_type = $_POST['proxy_type'];
$proxy_server = input('post.proxy_server', '', 'trim');
$proxy_port = input('post.proxy_port/d', 0);
$proxy_user = input('post.proxy_user', '', 'trim');
$proxy_pwd = input('post.proxy_pwd', '', 'trim');
$proxy_type = input('post.proxy_type', 'http', 'trim');
try {
check_proxy('https://dl.amh.sh/ip.htm', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd);
} catch (Exception $e) {

View File

@ -174,6 +174,44 @@ location / {
],
]
],
'litessl' => [
'name' => 'LiteSSL',
'class' => 1,
'icon' => 'litessl.ico',
'wildcard' => true,
'max_domains' => 100,
'cname' => true,
'note' => '<a href="https://freessl.cn/automation/eab-manager" target="_blank" rel="noreferrer">LiteSSL密钥获取</a>',
'inputs' => [
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => 'EAB申请邮箱',
'required' => true,
],
'kid' => [
'name' => 'EAB KID',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'key' => [
'name' => 'EAB HMAC Key',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
]
],
'tencent' => [
'name' => '腾讯云免费SSL',
'class' => 2,
@ -219,7 +257,7 @@ location / {
'wildcard' => false,
'max_domains' => 1,
'cname' => false,
'note' => '每个自然年有20张免费证书额度证书到期或吊销不释放额度。需要先进入阿里云控制台-<a href="https://yundun.console.aliyun.com/?p=cas#/certExtend/free/cn-hangzhou" target="_blank" rel="noreferrer">数字证书管理服务</a>,购买个人测试证书资源包。',
'note' => '每个自然年有20张免费证书额度证书到期或吊销不释放额度。需要先进入阿里云控制台-<a href="https://yundun.console.aliyun.com/?p=cas#/instance/TEST/cn-hangzhou" target="_blank" rel="noreferrer">数字证书管理服务</a>,购买个人测试证书资源包。',
'inputs' => [
'AccessKeyId' => [
'name' => 'AccessKeyId',

View File

@ -370,9 +370,8 @@ class DeployHelper
'id' => [
'name' => '证书ID',
'type' => 'input',
'placeholder' => '',
'placeholder' => '留空则为添加证书',
'note' => '在网站管理->证书管理查看证书的ID注意域名是否与证书匹配',
'required' => true,
],
],
],
@ -435,9 +434,8 @@ class DeployHelper
'id' => [
'name' => '证书ID',
'type' => 'input',
'placeholder' => '',
'placeholder' => '留空则为添加证书',
'note' => '在站点->证书管理查看证书的ID注意域名是否与证书匹配',
'required' => true,
],
],
],
@ -613,10 +611,10 @@ class DeployHelper
'show' => 'type==0',
],
'node_name' => [
'name' => '节点名称',
'name' => '节点名称',
'type' => 'textarea',
'placeholder' => '每行一个节点名称',
'note' => '不填写时,将替换主控节点证书;否则,将替换被控节点证书。多个节点请每行填写一个',
'placeholder' => '每行一个节点名称',
'note' => '不填写时:只更新主节点证书;填写时:同时更新主节点和所有指定的子节点证书。每行填写一个子节点名称',
'show' => 'type==0',
],
],
@ -679,6 +677,65 @@ class DeployHelper
],
],
],
'acepanel' => [
'name' => 'AcePanel',
'class' => 1,
'icon' => 'acepanel.svg',
'desc' => '支持 AcePanel 3.0+ 版本使用',
'note' => '支持 AcePanel 3.0+ 版本使用',
'inputs' => [
'url' => [
'name' => '面板地址',
'type' => 'input',
'placeholder' => 'AcePanel 地址',
'note' => '填写规则如https://192.168.1.100:8888/xxxxxx ,带访问入口但不要带其他后缀',
'required' => true,
],
'id' => [
'name' => '访问令牌ID',
'type' => 'input',
'placeholder' => '1',
'note' => 'AcePanel 设置->用户->访问令牌',
'required' => true,
],
'token' => [
'name' => '访问令牌',
'type' => 'input',
'note' => 'AcePanel 设置->用户->访问令牌',
'placeholder' => '32位字符串',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'taskinputs' => [
'type' => [
'name' => '部署类型',
'type' => 'radio',
'options' => [
'0' => 'AcePanel 网站的证书',
'1' => 'AcePanel 本身的证书',
],
'value' => '0',
'required' => true,
],
'sites' => [
'name' => '网站名称列表',
'type' => 'textarea',
'placeholder' => '填写要部署证书的网站名称,每行一个',
'note' => '填写创建网站时设置的网站唯一名称',
'show' => 'type==0',
'required' => true,
],
],
],
'ratpanel' => [
'name' => '耗子面板',
'class' => 1,
@ -779,6 +836,53 @@ class DeployHelper
],
],
],
'amh' => [
'name' => 'AMH面板',
'class' => 1,
'icon' => 'amh.ico',
'desc' => '',
'note' => null,
'tasknote' => '',
'inputs' => [
'url' => [
'name' => '面板地址',
'type' => 'input',
'placeholder' => 'AMH面板地址',
'note' => '填写规则如http://192.168.1.100:8888 ,不要带其他后缀',
'required' => true,
],
'apikey' => [
'name' => 'API接口密钥',
'type' => 'input',
'placeholder' => '安装amapi软件后查看是密钥不是私钥',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'taskinputs' => [
'env_name' => [
'name' => '环境名称',
'type' => 'input',
'placeholder' => '如lnmp01',
'required' => true,
],
'vhost_name' => [
'name' => '网站名称列表',
'type' => 'textarea',
'placeholder' => '填写要部署证书的网站标识域名,每行一个',
'note' => '网站标识域名一列的值,并非绑定域名',
'required' => true,
],
],
],
'synology' => [
'name' => '群晖面板',
'class' => 1,
@ -882,7 +986,7 @@ class DeployHelper
sudo visudo<br/>
#在文件最后一行增加以下内容需要将username替换成自己的用户名<br/>
username ALL=(ALL) NOPASSWD: NOPASSWD: ALL<br/>
ctrl+x 保存退出',
ctrl+x 保存退出<br/>',
'tasknote' => '系统会根据关联SSL证书的域名自动更新对应证书',
'inputs' => [
'host' => [
@ -1060,6 +1164,8 @@ ctrl+x 保存退出',
['value'=>'vod', 'label'=>'视频点播'],
['value'=>'fc', 'label'=>'函数计算3.0'],
['value'=>'fc2', 'label'=>'函数计算2.0'],
['value'=>'ga', 'label'=>'全球加速'],
['value'=>'upload', 'label'=>'上传到证书管理'],
],
'value' => 'cdn',
'required' => true,
@ -1149,6 +1255,21 @@ ctrl+x 保存退出',
'note' => '进入NLB实例详情->监听列表复制监听ID只支持TCPSSL监听协议',
'required' => true,
],
'ga_id' => [
'name' => '全球加速实例ID',
'type' => 'input',
'placeholder' => '',
'show' => 'product==\'ga\'',
'required' => true,
],
'ga_listener_id' => [
'name' => '监听ID',
'type' => 'input',
'placeholder' => '',
'show' => 'product==\'ga\'',
'note' => '进入实例详情->监听列表复制监听ID只支持HTTPS监听协议',
'required' => true,
],
'deploy_type' => [
'name' => '部署证书类型',
'type' => 'select',
@ -1157,21 +1278,21 @@ ctrl+x 保存退出',
['value'=>'1', 'label'=>'扩展证书'],
],
'value' => '0',
'show' => 'product==\'clb\'||product==\'alb\'||product==\'nlb\'',
'show' => 'product==\'clb\'||product==\'alb\'||product==\'nlb\'||product==\'ga\'',
'required' => true,
],
'clb_domain' => [
'name' => '扩展域名',
'type' => 'input',
'placeholder' => '多个域名可使用,分隔',
'show' => 'product==\'clb\'&&deploy_type==1',
'show' => 'product==\'clb\'&&deploy_type==1||product==\'ga\'&&deploy_type==1',
'required' => true,
],
'domain' => [
'name' => '绑定的域名',
'type' => 'input',
'placeholder' => '',
'show' => 'product!=\'esa\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'',
'show' => 'product!=\'esa\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'&&product!=\'ga\'&&product!=\'upload\'',
'required' => true,
],
],
@ -1224,6 +1345,7 @@ ctrl+x 保存退出',
['value'=>'tse', 'label'=>'云原生API网关TSE'],
['value'=>'tcb', 'label'=>'云开发TCB'],
['value'=>'lighthouse', 'label'=>'轻量应用服务器'],
['value'=>'upload', 'label'=>'上传到证书管理'],
['value'=>'update', 'label'=>'更新证书内容证书ID不变'],
],
'value' => 'cdn',
@ -1324,7 +1446,7 @@ ctrl+x 保存退出',
'name' => '绑定的域名',
'type' => 'input',
'placeholder' => '',
'show' => 'product!=\'clb\'&&product!=\'tke\'',
'show' => 'product!=\'clb\'&&product!=\'tke\'&&product!=\'upload\'',
'note' => 'CDN、EO、WAF多个域名可用,隔开其他只能填写1个域名',
'required' => true,
],
@ -1375,15 +1497,31 @@ ctrl+x 保存退出',
['value'=>'cdn', 'label'=>'内容分发网络CDN'],
['value'=>'elb', 'label'=>'弹性负载均衡ELB'],
['value'=>'waf', 'label'=>'Web应用防火墙WAF'],
['value'=>'obs', 'label'=>'对象存储服务OBS'],
['value'=>'upload', 'label'=>'上传到证书管理'],
],
'value' => 'cdn',
'required' => true,
],
'obs_endpoint' => [
'name' => 'Endpoint地址',
'type' => 'input',
'placeholder' => '填写示例obs.cn-north-4.myhuaweicloud.com',
'show' => 'product==\'obs\'',
'required' => true,
],
'obs_bucket' => [
'name' => '桶名称',
'type' => 'input',
'placeholder' => '',
'show' => 'product==\'obs\'',
'required' => true,
],
'domain' => [
'name' => '绑定的域名',
'type' => 'input',
'placeholder' => '多个域名可使用,分隔',
'show' => 'product==\'cdn\'',
'show' => 'product==\'cdn\'||product==\'obs\'',
'required' => true,
],
'project_id' => [
@ -1478,6 +1616,7 @@ ctrl+x 保存退出',
['value'=>'cdn', 'label'=>'CDN'],
['value'=>'oss', 'label'=>'OSS'],
['value'=>'pili', 'label'=>'视频直播'],
['value'=>'upload', 'label'=>'上传到证书管理'],
],
'value' => 'cdn',
'required' => true,
@ -1493,6 +1632,7 @@ ctrl+x 保存退出',
'name' => '绑定的域名',
'type' => 'input',
'placeholder' => '多个域名可使用,分隔',
'show' => 'product!=\'upload\'',
'required' => true,
],
],
@ -1603,6 +1743,7 @@ ctrl+x 保存退出',
['value'=>'cdn', 'label'=>'CDN'],
['value'=>'blb', 'label'=>'普通型BLB'],
['value'=>'appblb', 'label'=>'应用型BLB'],
['value'=>'upload', 'label'=>'上传到证书管理'],
],
'value' => 'cdn',
'required' => true,
@ -1737,6 +1878,7 @@ ctrl+x 保存退出',
['value'=>'tos', 'label'=>'对象存储TOS'],
['value'=>'live', 'label'=>'视频直播'],
['value'=>'imagex', 'label'=>'veImageX'],
['value'=>'upload', 'label'=>'上传到证书管理'],
],
'value' => 'cdn',
'required' => true,
@ -1752,7 +1894,7 @@ ctrl+x 保存退出',
'name' => '绑定的域名',
'type' => 'input',
'placeholder' => '多个域名可使用,分隔',
'show' => 'product!=\'clb\'&&product!=\'alb\'',
'show' => 'product!=\'clb\'&&product!=\'alb\'&&product!=\'upload\'',
'required' => true,
],
'listener_id' => [
@ -1948,10 +2090,22 @@ ctrl+x 保存退出',
['value'=>'cdn', 'label'=>'CDN加速'],
['value'=>'icdn', 'label'=>'全站加速'],
['value'=>'accessone', 'label'=>'边缘安全加速平台'],
['value'=>'cf', 'label'=>'函数计算'],
],
'value' => 'cdn',
'required' => true,
],
'region_id' => [
'name' => '所属地域',
'type' => 'select',
'options' => [
['value'=>'bb9fdb42056f11eda1610242ac110002', 'label'=>'华东1'],
['value'=>'200000002368', 'label'=>'西南1'],
],
'value' => 'bb9fdb42056f11eda1610242ac110002',
'show' => 'product==\'cf\'',
'required' => true,
],
'domain' => [
'name' => '绑定的域名',
'type' => 'input',
@ -2034,9 +2188,8 @@ ctrl+x 保存退出',
'id' => [
'name' => '证书ID',
'type' => 'input',
'placeholder' => '',
'placeholder' => '留空则为添加证书',
'note' => '在SSL证书->我的证书页面查看,注意域名是否与证书匹配',
'required' => true,
],
],
],

View File

@ -9,9 +9,30 @@ class DnsHelper
public static $dns_config = [
'aliyun' => [
'name' => '阿里云',
'icon' => 'aliyun.png',
'note' => '',
'config' => [
'ak' => 'AccessKeyId',
'sk' => 'AccessKeySecret',
'AccessKeyId' => [
'name' => 'AccessKeyId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'AccessKeySecret' => [
'name' => 'AccessKeySecret',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 1, //是否支持备注1单独设置备注2和记录一起设置
'status' => true, //是否支持启用暂停
@ -23,9 +44,30 @@ class DnsHelper
],
'dnspod' => [
'name' => '腾讯云',
'icon' => 'dnspod.ico',
'note' => '',
'config' => [
'ak' => 'SecretId',
'sk' => 'SecretKey',
'SecretId' => [
'name' => 'SecretId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'SecretKey' => [
'name' => 'SecretKey',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 1,
'status' => true,
@ -37,9 +79,30 @@ class DnsHelper
],
'huawei' => [
'name' => '华为云',
'icon' => 'huawei.ico',
'note' => '',
'config' => [
'ak' => 'AccessKeyId',
'sk' => 'SecretAccessKey',
'AccessKeyId' => [
'name' => 'AccessKeyId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'SecretAccessKey' => [
'name' => 'SecretAccessKey',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 2,
'status' => true,
@ -51,9 +114,30 @@ class DnsHelper
],
'baidu' => [
'name' => '百度云',
'icon' => 'baidu.ico',
'note' => '',
'config' => [
'ak' => 'AccessKey',
'sk' => 'SecretKey',
'AccessKeyId' => [
'name' => 'AccessKeyId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'SecretAccessKey' => [
'name' => 'SecretAccessKey',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 2,
'status' => false,
@ -65,9 +149,30 @@ class DnsHelper
],
'west' => [
'name' => '西部数码',
'icon' => 'west.ico',
'note' => '',
'config' => [
'ak' => '用户名',
'sk' => 'API密码',
'username' => [
'name' => '用户名',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'api_password' => [
'name' => 'API密码',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 0,
'status' => true,
@ -79,9 +184,30 @@ class DnsHelper
],
'huoshan' => [
'name' => '火山引擎',
'icon' => 'huoshan.ico',
'note' => '',
'config' => [
'ak' => 'AccessKeyId',
'sk' => 'SecretAccessKey',
'AccessKeyId' => [
'name' => 'AccessKeyId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'SecretAccessKey' => [
'name' => 'SecretAccessKey',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 2,
'status' => true,
@ -93,9 +219,30 @@ class DnsHelper
],
'jdcloud' => [
'name' => '京东云',
'icon' => 'jdcloud.ico',
'note' => '',
'config' => [
'ak' => 'AccessKeyId',
'sk' => 'AccessKeySecret',
'AccessKeyId' => [
'name' => 'AccessKeyId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'AccessKeySecret' => [
'name' => 'AccessKeySecret',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 0,
'status' => true,
@ -107,9 +254,30 @@ class DnsHelper
],
'dnsla' => [
'name' => 'DNSLA',
'icon' => 'dnsla.ico',
'note' => '',
'config' => [
'ak' => 'APIID',
'sk' => 'API密钥',
'apiid' => [
'name' => 'APIID',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'apisecret' => [
'name' => 'API密钥',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 0,
'status' => true,
@ -119,11 +287,117 @@ class DnsHelper
'page' => false,
'add' => true,
],
'qingcloud' => [
'name' => '青云',
'icon' => 'qingcloud.ico',
'note' => '',
'config' => [
'access_key_id' => [
'name' => 'Access Key ID',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'secret_access_key' => [
'name' => 'Secret Access Key',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 0,
'status' => true,
'redirect' => false,
'log' => false,
'weight' => true,
'page' => false,
'add' => false,
],
'bt' => [
'name' => '宝塔域名',
'icon' => 'bt.png',
'note' => '',
'config' => [
'AccessKey' => [
'name' => 'Access Key',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'SecretKey' => [
'name' => 'Secret Key',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'AccountID' => [
'name' => 'Account ID',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 2,
'status' => true,
'redirect' => false,
'log' => false,
'weight' => true,
'page' => false,
'add' => true,
],
'cloudflare' => [
'name' => 'Cloudflare',
'icon' => 'cloudflare.ico',
'note' => '',
'config' => [
'ak' => '邮箱地址',
'sk' => 'API密钥/令牌',
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'apikey' => [
'name' => 'API密钥/令牌',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'auth' => [
'name' => '认证方式',
'type' => 'radio',
'options' => [
'0' => 'API密钥',
'1' => 'API令牌',
],
'value' => '0'
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 2,
'status' => true,
@ -135,9 +409,30 @@ class DnsHelper
],
'namesilo' => [
'name' => 'NameSilo',
'icon' => 'namesilo.ico',
'note' => '',
'config' => [
'ak' => '账户名',
'sk' => 'API Key',
'username' => [
'name' => '账户名',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'apikey' => [
'name' => 'API Key',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 0,
'status' => false,
@ -147,12 +442,73 @@ class DnsHelper
'page' => true,
'add' => false,
],
'spaceship' => [
'name' => 'Spaceship',
'icon' => 'spaceship.ico',
'note' => '',
'config' => [
'apikey' => [
'name' => 'API Key',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'apisecret' => [
'name' => 'API Secret',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 0,
'status' => false,
'redirect' => true,
'log' => false,
'weight' => false,
'page' => false,
'add' => false,
],
'powerdns' => [
'name' => 'PowerDNS',
'icon' => 'powerdns.ico',
'note' => '',
'config' => [
'ak' => 'IP地址',
'sk' => '端口',
'ext' => 'API KEY',
'ip' => [
'name' => 'IP地址',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'port' => [
'name' => '端口',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'apikey' => [
'name' => 'API KEY',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 2,
'status' => true,
@ -162,19 +518,95 @@ class DnsHelper
'page' => true,
'add' => true,
],
'spaceship' => [
'name' => 'Spaceship',
'aliyunesa' => [
'name' => '阿里云ESA',
'icon' => 'aliyun.png',
'note' => '仅支持以NS方式接入阿里云ESA的域名',
'config' => [
'ak' => 'AccessKey',
'sk' => 'SecretKey',
'AccessKeyId' => [
'name' => 'AccessKeyId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'AccessKeySecret' => [
'name' => 'AccessKeySecret',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'region' => [
'name' => 'API接入点',
'type' => 'select',
'options' => [
['value' => 'cn-hangzhou', 'label' => '中国内地'],
['value' => 'ap-southeast-1', 'label' => '非中国内地'],
],
'value' => 'cn-hangzhou',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 0,
'remark' => 2,
'status' => false,
'redirect' => true,
'redirect' => false,
'log' => false,
'weight' => false,
'page' => false,
'add' => true,
'add' => false,
],
'tencenteo' => [
'name' => '腾讯云EO',
'icon' => 'tencent.png',
'note' => '仅支持以NS方式接入腾讯云EO的域名',
'config' => [
'SecretId' => [
'name' => 'SecretId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'SecretKey' => [
'name' => 'SecretKey',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'site_type' => [
'name' => 'API接入点',
'type' => 'select',
'options' => [
['value' => 'cn', 'label' => '中国内地'],
['value' => 'intl', 'label' => '非中国内地'],
],
'value' => 'cn',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 0,
'status' => true,
'redirect' => false,
'log' => false,
'weight' => true,
'page' => false,
'add' => false,
],
];
@ -187,10 +619,14 @@ class DnsHelper
'huoshan' => ['DEF' => 'default', 'CT' => 'telecom', 'CU' => 'unicom', 'CM' => 'mobile', 'AB' => 'oversea'],
'baidu' => ['DEF' => 'default', 'CT' => 'ct', 'CU' => 'cnc', 'CM' => 'cmnet', 'AB' => ''],
'jdcloud' => ['DEF' => '-1', 'CT' => '1', 'CU' => '2', 'CM' => '3', 'AB' => '4'],
'bt' => ['DEF' => '0', 'CT' => '285344768', 'CU' => '285345792', 'CM' => '285346816'],
'qingcloud' => ['DEF' => '0', 'CT' => '2', 'CU' => '3', 'CM' => '4', 'AB' => '8'],
'cloudflare' => ['DEF' => '0'],
'namesilo' => ['DEF' => 'default'],
'powerdns' => ['DEF' => 'default'],
'spaceship' => ['DEF' => 'default'],
'aliyunesa' => ['DEF' => '0'],
'tencenteo' => ['DEF' => 'Default'],
];
public static function getList()
@ -210,11 +646,12 @@ class DnsHelper
*/
public static function getModel($aid, $domain = null, $domainid = null)
{
$config = self::getConfig($aid);
if (!$config) return false;
$dnstype = $config['type'];
$account = self::getConfig($aid);
if (!$account) return false;
$dnstype = $account['type'];
$class = "\\app\\lib\\dns\\{$dnstype}";
if (class_exists($class)) {
$config = json_decode($account['config'], true);
$config['domain'] = $domain;
$config['domainid'] = $domainid;
$model = new $class($config);
@ -226,13 +663,14 @@ class DnsHelper
/**
* @return DnsInterface|bool
*/
public static function getModel2($config)
public static function getModel2($account)
{
$dnstype = $config['type'];
$dnstype = $account['type'];
$class = "\\app\\lib\\dns\\{$dnstype}";
if (class_exists($class)) {
$config['domain'] = $config['name'];
$config['domainid'] = $config['thirdid'];
$config = json_decode($account['config'], true);
$config['domain'] = $account['name'];
$config['domainid'] = $account['thirdid'];
$model = new $class($config);
return $model;
}

View File

@ -62,10 +62,14 @@ class customacme implements CertInterface
$dnsList = [];
if (!empty($order['challenges'])) {
$keys = [];
foreach ($order['challenges'] as $opts) {
$key = $opts['key'] . '|' .$opts['value'];
if (in_array($key, $keys)) continue;
$mainDomain = getMainDomain($opts['domain']);
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
$keys[] = $key;
}
}

120
app/lib/cert/litessl.php Normal file
View File

@ -0,0 +1,120 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\acme\ACMECert;
use Exception;
class litessl implements CertInterface
{
private $directory = 'https://acme.litessl.com/acme/v2/directory';
private $ac;
private $config;
private $ext;
public function __construct($config, $ext = null)
{
$this->config = $config;
$this->ac = new ACMECert($this->directory, (int)$config['proxy']);
if ($ext) {
$this->ext = $ext;
$this->ac->loadAccountKey($ext['key']);
$this->ac->setAccount($ext['kid']);
}
}
public function register()
{
if (empty($this->config['email'])) throw new Exception('邮件地址不能为空');
if (empty($this->config['kid']) || empty($this->config['key'])) {
throw new Exception('EAB密钥不能为空');
}
if (!empty($this->ext['key'])) {
$kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']);
return ['kid' => $kid, 'key' => $this->ext['key']];
}
$key = $this->ac->generateRSAKey(2048);
$this->ac->loadAccountKey($key);
$kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']);
return ['kid' => $kid, 'key' => $key];
}
public function buyCert($domainList, &$order)
{
}
public function createOrder($domainList, &$order, $keytype, $keysize)
{
$domain_config = [];
foreach ($domainList as $domain) {
if (empty($domain)) continue;
$domain_config[$domain] = ['challenge' => 'dns-01'];
}
if (empty($domain_config)) throw new Exception('域名列表不能为空');
$order = $this->ac->createOrder($domain_config);
$dnsList = [];
if (!empty($order['challenges'])) {
$keys = [];
foreach ($order['challenges'] as $opts) {
$key = $opts['key'] . '|' .$opts['value'];
if (in_array($key, $keys)) continue;
$mainDomain = getMainDomain($opts['domain']);
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
/*if (!array_key_exists($mainDomain, $dnsList)) {
$dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "litessl.cn"'];
}*/
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
$keys[] = $key;
}
}
return $dnsList;
}
public function authOrder($domainList, $order)
{
$this->ac->authOrder($order);
}
public function getAuthStatus($domainList, $order)
{
return true;
}
public function finalizeOrder($domainList, $order, $keytype, $keysize)
{
if (empty($domainList)) throw new Exception('域名列表不能为空');
if ($keytype == 'ECC') {
if (empty($keysize)) $keysize = '384';
$private_key = $this->ac->generateECKey($keysize);
} else {
if (empty($keysize)) $keysize = '2048';
$private_key = $this->ac->generateRSAKey($keysize);
}
$fullchain = $this->ac->finalizeOrder($domainList, $order, $private_key);
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $certInfo['issuer']['CN'], 'subject' => $certInfo['subject']['CN'], 'validFrom' => $certInfo['validFrom_time_t'], 'validTo' => $certInfo['validTo_time_t']];
}
public function revoke($order, $pem)
{
$this->ac->revoke($pem);
}
public function cancel($order)
{
}
public function setLogger($func)
{
$this->ac->setLogger($func);
}
}

View File

@ -30,7 +30,7 @@ class Ctyun
* @return array
* @throws Exception
*/
public function request($method, $path, $query = null, $params = null)
public function request($method, $path, $query = null, $params = null, $header = null)
{
if (!empty($query)) {
$query = array_filter($query, function ($a) { return $a !== null;});
@ -50,6 +50,11 @@ class Ctyun
if ($body) {
$headers['Content-Type'] = 'application/json';
}
if (!empty($header)) {
foreach ($header as $key => $value) {
$headers[$key] = $value;
}
}
$authorization = $this->generateSign($query, $headers, $body, $date);
$headers['Eop-Authorization'] = $authorization;
@ -151,7 +156,7 @@ class Ctyun
curl_close($ch);
$arr = json_decode($response, true);
if (isset($arr['statusCode']) && $arr['statusCode'] == 100000) {
if (isset($arr['statusCode']) && ($arr['statusCode'] == 100000 || $arr['statusCode'] == 0 && $this->endpoint == 'cf-global.ctapi.ctyun.cn')) {
return isset($arr['returnObj']) ? $arr['returnObj'] : true;
} elseif (isset($arr['errorMessage'])) {
throw new Exception($arr['errorMessage']);

View File

@ -0,0 +1,232 @@
<?php
namespace app\lib\client;
use Exception;
/**
* 华为云OBS
*/
class HuaweiOBS
{
private $AccessKeyId;
private $SecretAccessKey;
private $Endpoint;
private $proxy = false;
public function __construct($AccessKeyId, $SecretAccessKey, $Endpoint, $proxy = false)
{
$this->AccessKeyId = $AccessKeyId;
$this->SecretAccessKey = $SecretAccessKey;
$this->Endpoint = $Endpoint;
$this->proxy = $proxy;
}
public function setBucketCustomdomain($bucket, $domain, $cert_name, $fullchain, $privatekey)
{
$strXml = <<<EOF
<CustomDomainConfiguration>
</CustomDomainConfiguration>
EOF;
$xml = new \SimpleXMLElement($strXml);
$xml->addChild('Name', $cert_name);
$xml->addChild('Certificate', $fullchain);
$xml->addChild('PrivateKey', $privatekey);
$body = $xml->asXML();
$options = [
'bucket' => $bucket,
'key' => '',
];
$query = [
'customdomain' => $domain
];
return $this->request('PUT', '/', $query, $body, $options);
}
public function deleteBucketCustomdomain($bucket, $domain)
{
$options = [
'bucket' => $bucket,
'key' => '',
];
$query = [
'customdomain' => $domain
];
return $this->request('DELETE', '/', $query, '', $options);
}
public function getBucketCustomdomain($bucket)
{
$options = [
'bucket' => $bucket,
'key' => '',
];
$query = [
'customdomain' => '',
];
return $this->request('GET', '/', $query, '', $options);
}
private function request($method, $path, $query, $body, $options)
{
$hostname = $options['bucket'] . '.' . $this->Endpoint;
$query_string = $this->toQueryString($query);
$query_string = empty($query_string) ? '' : '?' . $query_string;
$requestUrl = 'https://' . $hostname . $path . $query_string;
$headers = [
'Content-Type' => 'application/xml',
'Content-MD5' => base64_encode(md5($body, true)),
'Date' => gmdate('D, d M Y H:i:s \G\M\T'),
];
$headers['Authorization'] = $this->getAuthorization($method, $path, $query, $headers, $options);
$header = [];
foreach ($headers as $key => $value) {
$header[] = $key . ': ' . $value;
}
return $this->curl($method, $requestUrl, $body, $header);
}
private function curl($method, $url, $body, $header)
{
$ch = curl_init($url);
if ($this->proxy) {
curl_set_proxy($ch);
}
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if (!empty($body)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . $errmsg);
}
curl_close($ch);
if ($httpCode >= 200 && $httpCode < 300) {
if (empty($response)) return true;
return $this->xml2array($response);
}
$arr = $this->xml2array($response);
if (isset($arr['Message'])) {
throw new Exception($arr['Message']);
} else {
throw new Exception('HTTP Code: ' . $httpCode);
}
}
private function toQueryString($params = array())
{
$temp = array();
uksort($params, 'strnatcasecmp');
foreach ($params as $key => $value) {
if (is_string($key) && !is_array($value)) {
if (strlen($value) > 0) {
$temp[] = rawurlencode($key) . '=' . rawurlencode($value);
} else {
$temp[] = rawurlencode($key);
}
}
}
return implode('&', $temp);
}
private function xml2array($xml)
{
if (!$xml) {
return false;
}
LIBXML_VERSION < 20900 && libxml_disable_entity_loader(true);
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
}
private function getAuthorization($method, $url, $query, $headers, $options)
{
$method = strtoupper($method);
$date = $headers['Date'];
$resourcePath = $this->getResourcePath($options);
$stringToSign = $this->calcStringToSign($method, $date, $headers, $resourcePath, $query);
$signature = base64_encode(hash_hmac('sha1', $stringToSign, $this->SecretAccessKey, true));
return 'OBS ' . $this->AccessKeyId . ':' . $signature;
}
private function getResourcePath(array $options)
{
$resourcePath = '/';
if (strlen($options['bucket']) > 0) {
$resourcePath .= $options['bucket'] . '/';
}
if (strlen($options['key']) > 0) {
$resourcePath .= $options['key'];
}
return $resourcePath;
}
private function calcStringToSign($method, $date, array $headers, $resourcePath, array $query)
{
/*
SignToString =
VERB + "\n"
+ Content-MD5 + "\n"
+ Content-Type + "\n"
+ Date + "\n"
+ CanonicalizedOSSHeaders
+ CanonicalizedResource
Signature = base64(hmac-sha1(AccessKeySecret, SignToString))
*/
$contentMd5 = '';
$contentType = '';
// CanonicalizedOSSHeaders
$signheaders = array();
foreach ($headers as $key => $value) {
$lowk = strtolower($key);
if (strncmp($lowk, "x-obs-", 6) == 0) {
$signheaders[$lowk] = $value;
} else if ($lowk === 'content-md5') {
$contentMd5 = $value;
} else if ($lowk === 'content-type') {
$contentType = $value;
}
}
ksort($signheaders);
$canonicalizedOSSHeaders = '';
foreach ($signheaders as $key => $value) {
$canonicalizedOSSHeaders .= $key . ':' . $value . "\n";
}
// CanonicalizedResource
$signquery = array();
foreach ($query as $key => $value) {
if (in_array($key, $this->signKeyList)) {
$signquery[$key] = $value;
}
}
ksort($signquery);
$sortedQueryList = array();
foreach ($signquery as $key => $value) {
if (strlen($value) > 0) {
$sortedQueryList[] = $key . '=' . $value;
} else {
$sortedQueryList[] = $key;
}
}
$queryStringSorted = implode('&', $sortedQueryList);
$canonicalizedResource = $resourcePath;
if (!empty($queryStringSorted)) {
$canonicalizedResource .= '?' . $queryStringSorted;
}
return $method . "\n" . $contentMd5 . "\n" . $contentType . "\n" . $date . "\n" . $canonicalizedOSSHeaders . $canonicalizedResource;
}
private $signKeyList = array(
'acl', 'policy', 'torrent', 'logging', 'location', 'storageinfo', 'quota', 'storagepolicy', 'requestpayment', 'versions', 'versioning', 'versionid', 'uploads', 'uploadid', 'partnumber', 'website', 'notification', 'lifecycle', 'deletebucket', 'delete', 'cors', 'restore', 'tagging', 'response-content-type', 'response-content-language', 'response-expires', 'response-cache-control', 'response-content-disposition', 'response-content-encoding', 'x-image-process', 'backtosource', 'storageclass', 'replication', 'append', 'position', 'x-oss-process', 'CDNNotifyConfiguration', 'attname', 'customdomain', 'directcoldaccess', 'encryption', 'inventory', 'length', 'metadata', 'modify', 'name', 'rename', 'truncate', 'x-image-save-bucket', 'x-image-save-object', 'x-obs-security-token', 'x-obs-callback'
);
}

163
app/lib/deploy/acepanel.php Normal file
View File

@ -0,0 +1,163 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class acepanel implements DeployInterface
{
private $logger;
private $url;
private $id;
private $token;
private $proxy;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->id = $config['id'];
$this->token = $config['token'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->id) || empty($this->token)) throw new Exception('请填写完整面板地址和访问令牌');
$response = $this->request('/user/info', null, 'GET');
$result = json_decode($response, true);
if (isset($result['msg']) && $result['msg'] == "success") {
return true;
} else {
throw new Exception($result['msg'] ?? '面板地址无法连接');
}
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
if ($config['type'] == '1') {
$this->deployPanel($fullchain, $privatekey);
$this->log("面板证书部署成功");
return;
}
$sites = explode("\n", $config['sites']);
$success = 0;
$errmsg = null;
foreach ($sites as $site) {
$site = trim($site);
if (empty($site)) continue;
try {
$this->deploySite($site, $fullchain, $privatekey);
$this->log("网站 {$site} 证书部署成功");
$success++;
} catch (Exception $e) {
$errmsg = $e->getMessage();
$this->log("网站 {$site} 证书部署失败:" . $errmsg);
}
}
if ($success == 0) {
throw new Exception($errmsg ?: '要部署的网站不存在');
}
}
private function deployPanel($fullchain, $privatekey)
{
$data = [
'cert' => $fullchain,
'key' => $privatekey,
];
$response = $this->request('/setting/cert', $data);
$result = json_decode($response, true);
if (isset($result['msg']) && $result['msg'] == "success") {
return true;
} elseif (isset($result['msg'])) {
throw new Exception($result['msg']);
} else {
throw new Exception($response ?: '返回数据解析失败');
}
}
private function deploySite($name, $fullchain, $privatekey)
{
$data = [
'name' => $name,
'cert' => $fullchain,
'key' => $privatekey,
];
$response = $this->request('/website/cert', $data);
$result = json_decode($response, true);
if (isset($result['msg']) && $result['msg'] == "success") {
return true;
} elseif (isset($result['msg'])) {
throw new Exception($result['msg']);
} else {
throw new Exception($response ?: '返回数据解析失败');
}
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
private function request($path, $params, $method = 'POST')
{
$url = $this->url . '/api' . $path;
$body = $method == 'GET' ? null : json_encode($params);
$sign = $this->signRequest($method, $url, $body, $this->id, $this->token);
$response = http_request($url, $body, null, null, [
'Content-Type' => 'application/json',
'X-Timestamp' => $sign['timestamp'],
'Authorization' => 'HMAC-SHA256 Credential=' . $sign['id'] . ', Signature=' . $sign['signature']
], $this->proxy, $method);
return $response['body'];
}
private function signRequest($method, $url, $body, $id, $token)
{
// 解析URL并获取路径
$parsedUrl = parse_url($url);
$path = $parsedUrl['path'];
$query = $parsedUrl['query'] ?? '';
// 规范化路径
$canonicalPath = $path;
if (!str_starts_with($path, '/api')) {
$apiPos = strpos($path, '/api');
if ($apiPos !== false) {
$canonicalPath = substr($path, $apiPos);
}
}
// 构造规范化请求
$canonicalRequest = implode("\n", [
$method,
$canonicalPath,
$query,
hash('sha256', $body ?: '')
]);
// 计算签名
$timestamp = time();
$stringToSign = implode("\n", [
'HMAC-SHA256',
$timestamp,
hash('sha256', $canonicalRequest)
]);
$signature = hash_hmac('sha256', $stringToSign, $token);
return [
'timestamp' => $timestamp,
'signature' => $signature,
'id' => $id
];
}
}

View File

@ -66,6 +66,9 @@ class aliyun implements DeployInterface
$this->deploy_alb($cert_id, $config);
} elseif ($config['product'] == 'nlb') {
$this->deploy_nlb($cert_id, $config);
} elseif ($config['product'] == 'ga') {
$this->deploy_ga($cert_id, $config);
} elseif ($config['product'] == 'upload') {
} else {
throw new Exception('未知的产品类型');
}
@ -200,11 +203,11 @@ class aliyun implements DeployInterface
}
$this->log('ESA站点 ' . $sitename . ' 查询到' . $data['TotalCount'] . '个SSL证书');
$exist_cert_id = null;
$exist_cert_name = null;
$exist_cert_casid = null;
$exist_cert = null;
$oldest_cert = null;
if ($data['TotalCount'] > 0) {
foreach ($data['Result'] as $cert) {
if ($cert['Type'] == 'free') continue;
$domains = explode(',', $cert['SAN']);
$flag = true;
foreach ($domains as $domain) {
@ -214,11 +217,40 @@ class aliyun implements DeployInterface
}
}
if ($flag) {
$exist_cert_id = $cert['Id'];
$exist_cert_name = $cert['Name'];
$exist_cert_casid = isset($cert['CasId']) ? $cert['CasId'] : null;
$exist_cert = $cert;
break;
}
if (!$oldest_cert) {
$oldest_cert = $cert;
} elseif (strtotime($cert['CreateTime']) < strtotime($oldest_cert['CreateTime'])) {
$oldest_cert = $cert;
}
}
}
if (!$exist_cert) { //新增证书时,若配额已满,则删除最旧的证书
$param = [
'Action' => 'ListInstanceQuotasWithUsage',
'SiteId' => $site_id,
'QuotaNames' => 'customHttpCert',
];
try {
$data = $client->request($param, 'GET');
} catch (Exception $e) {
throw new Exception('查询ESA站点证书配额失败' . $e->getMessage());
}
if (!empty($data['Quotas']) && intval($data['Quotas'][0]['Usage']) >= intval($data['Quotas'][0]['QuotaValue']) && $oldest_cert) {
$param = [
'Action' => 'DeleteCertificate',
'SiteId' => $site_id,
'Id' => $oldest_cert['Id'],
];
try {
$client->request($param, 'GET');
$this->log('ESA站点 ' . $sitename . ' 删除证书 ' . $oldest_cert['Name'] . ' 成功');
} catch (Exception $e) {
throw new Exception('ESA站点 ' . $sitename . ' 删除证书' . $oldest_cert['Name'] . '失败:' . $e->getMessage());
}
}
}
@ -231,10 +263,10 @@ class aliyun implements DeployInterface
'Region' => $config['region'],
];
if ($exist_cert_id) {
$param['Id'] = $exist_cert_id;
if ($exist_cert) {
$param['Id'] = $exist_cert['Id'];
if ($exist_cert_casid == $cas_id) {
if (isset($exist_cert['CasId']) && $exist_cert['CasId'] == $cas_id) {
$this->log('ESA站点 ' . $sitename . ' 证书已配置,无需重复操作');
return;
}
@ -242,8 +274,8 @@ class aliyun implements DeployInterface
$client->request($param);
if ($exist_cert_name) {
$this->log('ESA站点 ' . $sitename . ' 证书 ' . $exist_cert_name . ' 更新成功');
if ($exist_cert) {
$this->log('ESA站点 ' . $sitename . ' 证书 ' . $exist_cert['Name'] . ' 更新成功');
} else {
$this->log('ESA站点 ' . $sitename . ' 证书添加成功!');
}
@ -734,6 +766,84 @@ class aliyun implements DeployInterface
}
}
private function deploy_ga($cert_id, $config)
{
if (empty($config['ga_id'])) throw new Exception('全球加速实例ID不能为空');
if (empty($config['ga_listener_id'])) throw new Exception('全球加速监听ID不能为空');
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'ga.cn-hangzhou.aliyuncs.com', '2019-11-20', $this->proxy);
$cert_id = $cert_id . '-cn-hangzhou';
$deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0;
if ($deploy_type == 1) {
if (empty($config['clb_domain'])) throw new Exception('扩展域名不能为空');
$param = [
'Action' => 'ListListenerCertificates',
'RegionId' => 'cn-hangzhou',
'AcceleratorId' => $config['ga_id'],
'ListenerId' => $config['ga_listener_id'],
];
try {
$data = $client->request($param);
} catch (Exception $e) {
throw new Exception('扩展域名列表查询失败:' . $e->getMessage());
}
$need_add = [];
foreach (explode(',', $config['clb_domain']) as $domain) {
$domainExists = false;
$exist_cert_id = null;
foreach ($data['Certificates'] as $cert) {
if (isset($cert['Domain']) && $domain == $cert['Domain']) {
$domainExists = true;
$exist_cert_id = $cert['CertificateId'];
}
}
if ($domainExists) {
if ($exist_cert_id == $cert_id) {
$this->log('全球加速实例监听扩展域名 ' . $domain . ' 证书已配置');
continue;
}
$param = [
'Action' => 'UpdateAdditionalCertificateWithListener',
'RegionId' => 'cn-hangzhou',
'AcceleratorId' => $config['ga_id'],
'ListenerId' => $config['ga_listener_id'],
'Domain' => $domain,
'CertificateId' => $cert_id,
];
$client->request($param);
$this->log('全球加速实例监听扩展域名 ' . $domain . ' 替换证书成功!');
} else {
$need_add[] = $domain;
}
}
if (count($need_add) > 0) {
$param = [
'Action' => 'AssociateAdditionalCertificatesWithListener',
'RegionId' => 'cn-hangzhou',
'AcceleratorId' => $config['ga_id'],
'ListenerId' => $config['ga_listener_id'],
];
foreach ($need_add as $index => $domain) {
$param['Certificates.' . ($index + 1) . '.Id'] = $cert_id;
$param['Certificates.' . ($index + 1) . '.Domain'] = $domain;
}
$client->request($param);
$this->log('全球加速实例监听扩展域名 ' . implode(',', $need_add) . ' 绑定证书成功!');
}
} else {
$param = [
'Action' => 'UpdateListener',
'RegionId' => 'cn-hangzhou',
'AcceleratorId' => $config['ga_id'],
'ListenerId' => $config['ga_listener_id'],
'Certificates.1.Id' => $cert_id,
];
$client->request($param);
$this->log('全球加速实例监听默认证书更新成功!');
}
}
public function setLogger($func)
{
$this->logger = $func;

108
app/lib/deploy/amh.php Normal file
View File

@ -0,0 +1,108 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class amh implements DeployInterface
{
private $logger;
private $url;
private $apikey;
private $proxy;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->apikey = $config['apikey'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->apikey)) throw new Exception('请填写面板地址和接口密钥');
$this->login();
return true;
}
private function login()
{
$path = '/?c=amapi&a=login';
$post_data = 'amapi_expires=' . time() + 120;
$post_data .= '&amapi_sign=' . hash_hmac('sha256', $post_data, $this->apikey);
$response = $this->request($path, $post_data);
if ($response['code'] == 302 && strpos($response['redirect_url'], 'amh_token=') !== false) {
if(preg_match('/amh_token=([A-Za-z0-9]+)/', $response['redirect_url'], $matches)) {
return $matches[1];
}else{
throw new Exception('面板返回数据异常');
}
} elseif ($response['code'] == 200 && preg_match('/<p id="error".*?>(.*?)<\/p>/s', $response['body'], $matches)) {
throw new Exception(strip_tags($matches[1]));
} else {
throw new Exception('面板地址无法连接');
}
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
if (empty($config['env_name'])) throw new Exception('环境名称不能为空');
if (empty($config['vhost_name'])) throw new Exception('网站标识域名不能为空');
$amh_token = $this->login();
foreach (explode("\n", $config['vhost_name']) as $vhost_name) {
$vhost_name = trim($vhost_name);
if (empty($vhost_name)) continue;
$path = '/?c=amssl&a=admin_amssl&envs_name=' . $config['env_name'] . '&vhost_name=' . $vhost_name . '&ModuleSort=app';
$params = [
'submit_key_crt' => 'y',
'key_input1' => 'key_input1',
'key_content1' => $privatekey,
'crt_input1' => 'crt_input1',
'crt_content1' => $fullchain,
'amh_token' => $amh_token,
];
$response = $this->request($path, $params);
if (strpos($response['body'], '<p id="success"') !== false) {
$this->log("网站 {$vhost_name} 证书部署成功");
} elseif (preg_match('/<p id="error".*?>(.*?)<\/p>/s', $response['body'], $matches)) {
$errmsg = strip_tags($matches[1]);
$this->log("网站 {$vhost_name} 证书部署失败:" . $errmsg);
throw new Exception($errmsg);
} elseif (preg_match('/<p id="error".*?>(.*?)<br \/>/s', $response['body'], $matches)) {
$errmsg = $matches[1];
if (strpos($errmsg, '<br />') !== false) {
$errmsg = explode('<br />', $errmsg)[0];
}
$errmsg = strip_tags($errmsg);
$this->log("网站 {$vhost_name} 证书部署失败:" . $errmsg);
throw new Exception($errmsg);
} else {
throw new Exception("网站 {$vhost_name} 证书部署失败:未知错误");
}
}
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
private function request($path, $post_data = null)
{
$url = $this->url . $path;
$cookie = 'PHPSESSID=' . hash_hmac('md5', 'php_sessid=' . $this->apikey, $this->apikey);
$response = http_request($url, $post_data, null, $cookie, null, $this->proxy);
return $response;
}
}

View File

@ -39,6 +39,7 @@ class baidu implements DeployInterface
$this->deploy_blb($cert_id, $config);
} elseif ($config['product'] == 'appblb') {
$this->deploy_appblb($cert_id, $config);
} elseif ($config['product'] == 'upload') {
} else {
throw new Exception('不支持的产品类型');
}

View File

@ -43,7 +43,39 @@ class cdnfly implements DeployInterface
public function deploy($fullchain, $privatekey, $config, &$info)
{
$id = $config['id'];
if (empty($id)) throw new Exception('证书ID不能为空');
if (empty($id)) {
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
$cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
$params = [
'type' => 'custom',
'name' => $cert_name,
'cert' => $fullchain,
'key' => $privatekey,
];
if ($this->auth == 1) {
$access_token = $this->login();
$url = $this->url . '/v1/certs';
$body = json_encode($params);
$headers = [
'Access-Token' => $access_token,
];
$response = http_request($url, $body, null, null, $headers, $this->proxy, 'POST');
$result = json_decode($response['body'], true);
if (isset($result['code']) && $result['code'] == 0) {
$id = $result['data'];
} elseif (isset($result['msg'])) {
throw new Exception('证书添加失败,' . $result['msg']);
} else {
throw new Exception('证书添加失败,返回数据解析失败');
}
} else {
$id = $this->request('/v1/certs', $params, 'POST');
}
$this->log("证书ID:{$id}添加成功!");
$info['config']['id'] = $id;
return;
}
$params = [
'type' => 'custom',

View File

@ -39,6 +39,8 @@ class ctyun implements DeployInterface
$this->deploy_icdn($fullchain, $privatekey, $config);
} elseif ($config['product'] == 'accessone') {
$this->deploy_accessone($fullchain, $privatekey, $config);
} elseif ($config['product'] == 'cf') {
$this->deploy_cf($fullchain, $privatekey, $config);
}
}
@ -160,7 +162,7 @@ class ctyun implements DeployInterface
}
}
try {
$client->request('POST', '/ctapi/v1/accessone/domain/modify_config', null, $result);
$client->request('POST', '/ctapi/v1/scdn/domain/modify_config', null, $result);
} catch (Exception $e) {
if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) {
throw new Exception($e->getMessage());
@ -170,6 +172,44 @@ class ctyun implements DeployInterface
$this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!');
}
private function deploy_cf($fullchain, $privatekey, $config)
{
$client = new CtyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cf-global.ctapi.ctyun.cn', $this->proxy);
try {
$data = $client->request('GET', '/openapi/v1/domains/customdomains/' . $config['domain'], null, null, ['regionId' => $config['region_id']]);
} catch (Exception $e) {
throw new Exception('获取自定义域名配置失败:' . $e->getMessage());
}
if (isset($data['certConfig']['certificate']) && trim($data['certConfig']['certificate']) == trim($fullchain)) {
$this->log('函数计算域名 ' . $config['domain'] . ' 证书已部署,无需重复操作!');
return;
}
if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS';
$param = [
'domainName' => $config['domain'],
'description' => $data['description'],
'protocol' => $data['protocol'],
'certConfig' => [
'certName' => 'cert' . substr($config['cert_name'], strpos($config['cert_name'], '-') + 1),
'certificate' => $fullchain,
'privateKey' => $privatekey,
],
'authConfig' => $data['authConfig'],
'routeConfig' => $data['routeConfig'],
];
try {
$client->request('PUT', '/openapi/v1/domains/customdomains/' . $config['domain'], null, $param, ['regionId' => $config['region_id']]);
} catch (Exception $e) {
if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) {
throw new Exception($e->getMessage());
}
}
$this->log('函数计算域名 ' . $config['domain'] . ' 部署证书成功!');
}
public function setLogger($func)
{
$this->logger = $func;

View File

@ -52,7 +52,7 @@ class fnos implements DeployInterface
$this->exec($connection, '上传证书文件', "sudo tee ".$certPath." > /dev/null <<'EOF'\n".$fullchain."\nEOF");
$this->exec($connection, '上传私钥文件', "sudo tee ".$keyPath." > /dev/null <<'EOF'\n".$privatekey."\nEOF");
$this->exec($connection, '刷新目录权限', 'sudo chmod 0755 "'.$certDir.'" -R');
$this->exec($connection, '更新数据表', 'sudo -u postgres psql -d trim_connect -c "UPDATE cert SET valid_to='.$certInfo['validTo_time_t'].'000,valid_from='.$certInfo['validFrom_time_t'].'000,issued_by=\''.$certInfo['issuer']['CN'].'\',updated_time='.getMillisecond().' WHERE private_key=\''.$keyPath.'\'"');
$this->exec($connection, '更新数据表', 'cd /tmp && sudo -u postgres psql -d trim_connect -c "UPDATE cert SET valid_to='.$certInfo['validTo_time_t'].'000,valid_from='.$certInfo['validFrom_time_t'].'000,issued_by=\''.$certInfo['issuer']['CN'].'\',updated_time='.getMillisecond().' WHERE private_key=\''.$keyPath.'\'"');
$this->log('证书 '.$row['domain'].' 更新成功');
$success++;
}

View File

@ -4,6 +4,7 @@ namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\HuaweiCloud;
use app\lib\client\HuaweiOBS;
use Exception;
class huawei implements DeployInterface
@ -39,6 +40,11 @@ class huawei implements DeployInterface
$this->deploy_elb($fullchain, $privatekey, $config);
} elseif ($config['product'] == 'waf') {
$this->deploy_waf($fullchain, $privatekey, $config);
} elseif ($config['product'] == 'obs') {
$this->deploy_obs($fullchain, $privatekey, $config);
} elseif ($config['product'] == 'upload') {
$cert_id = $this->get_cert_id($fullchain, $privatekey);
$info['cert_id'] = $cert_id;
}
}
@ -117,6 +123,19 @@ class huawei implements DeployInterface
$this->log('WAF证书ID ' . $config['cert_id'] . ' 更新证书成功!');
}
private function deploy_obs($fullchain, $privatekey, $config)
{
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
if (empty($config['obs_endpoint'])) throw new Exception('OBS Endpoint不能为空');
if (empty($config['obs_bucket'])) throw new Exception('OBS 桶名称不能为空');
$obsClient = new HuaweiOBS($this->AccessKeyId, $this->SecretAccessKey, $config['obs_endpoint'], $this->proxy);
foreach (explode(',', $config['domain']) as $domain) {
if (empty($domain)) continue;
$obsClient->setBucketCustomdomain($config['obs_bucket'], $domain, $config['cert_name'], $fullchain, $privatekey);
$this->log('OSS域名 ' . $domain . ' 部署证书成功!');
}
}
private function get_cert_id($fullchain, $privatekey)
{
$certInfo = openssl_x509_parse($fullchain, true);

View File

@ -191,7 +191,7 @@ class huoshan implements DeployInterface
if (!$certInfo) throw new Exception('证书解析失败');
$cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
$client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'certificate_service', '2024-10-01', 'cn-beijing', $this->proxy);
$client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'certificate-service.volcengineapi.com', 'certificate_service', '2024-10-01', 'cn-beijing', $this->proxy);
$param = [
'Tag' => $cert_name,
'Repeatable' => false,
@ -207,10 +207,20 @@ class huoshan implements DeployInterface
}
if (!empty($data['InstanceId'])) {
$cert_id = $data['InstanceId'];
$this->log('上传证书成功 CertId=' . $cert_id);
$param = [
'InstanceId' => $cert_id,
'Options' => [
'ExpiredNotice' => 'Disabled',
],
];
$client->request('POST', 'CertificateUpdateInstance', $param);
} else {
$cert_id = $data['RepeatId'];
$this->log('找到已上传的证书 CertId=' . $cert_id);
}
$this->log('上传证书成功 CertId=' . $cert_id);
return $cert_id;
}

View File

@ -70,7 +70,7 @@ class kuocai implements DeployInterface
private function request($path, $params = null, $json = false)
{
$url = 'https://kuocai.cn' . $path;
$url = 'https://www.kuocaicdn.com' . $path;
$body = $json ? json_encode($params) : $params;
$headers = [];
if ($json) $headers['Content-Type'] = 'application/json';

View File

@ -41,13 +41,29 @@ class lecdn implements DeployInterface
public function deploy($fullchain, $privatekey, $config, &$info)
{
$id = $config['id'];
if (empty($id)) throw new Exception('证书ID不能为空');
if ($this->auth == 0) {
$this->login();
}
$id = $config['id'];
if (empty($id)) {
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
$cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
$params = [
'name' => $cert_name,
'type' => 'upload',
'ssl_pem' => base64_encode($fullchain),
'ssl_key' => base64_encode($privatekey),
'auto_renewal' => false,
];
$data = $this->request('/prod-api/certificate', $params, 'POST');
$id = $data['id'];
$this->log("证书ID:{$id}添加成功!");
$info['config']['id'] = $id;
return;
}
try {
$data = $this->request('/prod-api/certificate/' . $id);
} catch (Exception $e) {

View File

@ -41,7 +41,7 @@ class opanel implements DeployInterface
];
if (empty($nodeNames)) {
// 没有指定节点,部署到主控节点
// 没有指定节点,部署到主控节点
try {
$this->request('/core/settings/ssl/update', $params);
$this->log("面板证书更新成功!");
@ -50,9 +50,21 @@ class opanel implements DeployInterface
throw new Exception("面板证书更新失败:" . $e->getMessage());
}
} else {
// 部署到多个子节点
// 同时部署到主节点和所有指定的子节点
$successCount = 0;
$failCount = 0;
// 先更新主节点
try {
$this->request('/core/settings/ssl/update', $params);
$this->log("主节点面板证书更新成功!");
$successCount++;
} catch (Exception $e) {
$this->log("主节点面板证书更新失败:" . $e->getMessage());
$failCount++;
}
// 然后更新所有子节点
foreach ($nodeNames as $nodeName) {
try {
$this->request('/core/settings/ssl/update', $params, $nodeName);
@ -70,13 +82,24 @@ class opanel implements DeployInterface
}
}
// 如果没有指定节点,则部署到主控节点
// 如果没有指定节点,则部署到主控节点
if (empty($nodeNames)) {
$this->deployToNode($fullchain, $privatekey, $config, null);
} else {
// 部署到多个子节点
// 同时部署到主节点和所有指定的子节点
$successCount = 0;
$failCount = 0;
// 先更新主节点
try {
$this->deployToNode($fullchain, $privatekey, $config, null);
$successCount++;
} catch (Exception $e) {
$this->log("主节点部署失败:" . $e->getMessage());
$failCount++;
}
// 然后更新所有子节点
foreach ($nodeNames as $nodeName) {
try {
$this->deployToNode($fullchain, $privatekey, $config, $nodeName);
@ -228,6 +251,7 @@ class opanel implements DeployInterface
'1Panel-Token' => $token,
'1Panel-Timestamp' => $timestamp,
];
// 只有子节点时才设置 CurrentNode 头,主节点时不设置该头
if (!empty($nodeName)) {
$headers['CurrentNode'] = $nodeName;
}

View File

@ -37,6 +37,9 @@ class qiniu implements DeployInterface
$cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
$cert_id = $this->get_cert_id($fullchain, $privatekey, $certInfo['subject']['CN'], $cert_name);
$info['cert_id'] = $cert_id;
$info['cert_name'] = $cert_name;
if ($config['product'] == 'upload') return;
foreach (explode(',', $domains) as $domain) {
if (empty($domain)) continue;
@ -50,8 +53,6 @@ class qiniu implements DeployInterface
throw new Exception('未知的产品类型');
}
}
$info['cert_id'] = $cert_id;
$info['cert_name'] = $cert_name;
}
private function deploy_cdn($domain, $cert_id)

View File

@ -26,19 +26,43 @@ class rainyun implements DeployInterface
public function deploy($fullchain, $privatekey, $config, &$info)
{
if (empty($config['id'])) throw new Exception('证书ID不能为空');
if (empty($config['id'])) {
$params = [
'cert' => $fullchain,
'key' => $privatekey,
];
try {
$this->request('/product/sslcenter/', $params, 'POST');
} catch (Exception $e) {
throw new Exception('上传证书失败,' . $e->getMessage());
}
$params = [
'cert' => $fullchain,
'key' => $privatekey,
];
try {
$this->request('/product/sslcenter/' . $config['id'], $params, 'PUT');
} catch (Exception $e) {
throw new Exception($e->getMessage());
$params = [
'options' => '{"columnFilters":{"Domain":""},"sort":[],"page":1,"perPage":1}',
];
try {
$data = $this->request('/product/sslcenter/?' . http_build_query($params), null, 'GET');
} catch (Exception $e) {
throw new Exception('获取证书列表失败,' . $e->getMessage());
}
if (empty($data['Records'])) throw new Exception('未找到已上传的证书');
$cert_id = $data['Records'][0]['ID'];
$info['config']['id'] = $cert_id;
$this->log('证书ID:' . $cert_id . '添加成功!');
} else {
$params = [
'cert' => $fullchain,
'key' => $privatekey,
];
try {
$this->request('/product/sslcenter/' . $config['id'], $params, 'PUT');
} catch (Exception $e) {
throw new Exception($e->getMessage());
}
$this->log('证书ID:' . $config['id'] . '更新成功!');
}
$this->log('证书ID:' . $config['id'] . '更新成功!');
}
private function request($path, $params = null, $method = null)
@ -55,7 +79,7 @@ class rainyun implements DeployInterface
$response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
$result = json_decode($response['body'], true);
if (isset($result['code']) && $result['code'] == 200) {
return $result;
return isset($result['data']) ? $result['data'] : null;
} elseif (isset($result['message'])) {
throw new Exception($result['message']);
} else {

View File

@ -159,14 +159,20 @@ class ssh implements DeployInterface
file_put_contents($privateKeyPath, $this->config['privatekey']);
file_put_contents($publicKeyPath, $publicKey);
umask($umask);
if (!empty($this->config['passphrase'])) {
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $this->config['passphrase'])) {
throw new Exception('私钥认证失败');
}
} else {
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) {
throw new Exception('私钥认证失败');
try {
if (!empty($this->config['passphrase'])) {
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $this->config['passphrase'])) {
throw new Exception('私钥认证失败');
}
} else {
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) {
throw new Exception('私钥认证失败');
}
}
} finally {
unlink($publicKeyPath);
unlink($privateKeyPath);
}
} else {
if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) {

View File

@ -36,6 +36,7 @@ class tencent implements DeployInterface
}
$cert_id = $this->get_cert_id($fullchain, $privatekey);
if (!$cert_id) throw new Exception('证书ID获取失败');
$info['cert_id'] = $cert_id;
if ($config['product'] == 'cos') {
if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空');
if (empty($config['cos_bucket'])) throw new Exception('存储桶名称不能为空');
@ -65,6 +66,8 @@ class tencent implements DeployInterface
return $this->deploy_scf($cert_id, $config);
} elseif ($config['product'] == 'teo' && isset($config['site_id'])) {
return $this->deploy_teo($cert_id, $config);
} elseif ($config['product'] == 'upload') {
return;
} else {
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
if ($config['product'] == 'waf') {
@ -77,7 +80,6 @@ class tencent implements DeployInterface
}
try {
$record_id = $this->deploy_common($config['product'], $cert_id, $instance_id);
$info['cert_id'] = $cert_id;
$info['record_id'] = $record_id;
} catch (Exception $e) {
if (isset($info['record_id'])) {

View File

@ -92,8 +92,11 @@ class upyun implements DeployInterface
}
}
if ($i == 0) throw new Exception('未找到可迁移的证书');
$this->log('共迁移' . $i . '个证书,关联域名' . $d . '个');
if ($i == 0) {
$this->log('未找到可迁移的证书');
} else {
$this->log('共迁移' . $i . '个证书,关联域名' . $d . '个');
}
}
private function login()

View File

@ -20,8 +20,8 @@ class aliyun implements DnsInterface
public function __construct($config)
{
$this->AccessKeyId = $config['ak'];
$this->AccessKeySecret = $config['sk'];
$this->AccessKeyId = $config['AccessKeyId'];
$this->AccessKeySecret = $config['AccessKeySecret'];
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $this->Endpoint, $this->Version, $proxy);
$this->domain = $config['domain'];

284
app/lib/dns/aliyunesa.php Normal file
View File

@ -0,0 +1,284 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
use app\lib\client\Aliyun as AliyunClient;
use Exception;
class aliyunesa implements DnsInterface
{
private $AccessKeyId;
private $AccessKeySecret;
private $Endpoint = 'esa.cn-hangzhou.aliyuncs.com'; //API接入域名
private $Version = '2024-09-10'; //API版本号
private $error;
private $domain;
private $domainid;
private AliyunClient $client;
public function __construct($config)
{
$this->AccessKeyId = $config['AccessKeyId'];
$this->AccessKeySecret = $config['AccessKeySecret'];
if (!empty($config['region'])) {
$this->Endpoint = 'esa.'.$config['region'].'.aliyuncs.com';
}
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $this->Endpoint, $this->Version, $proxy);
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
}
public function getError()
{
return $this->error;
}
public function check()
{
if ($this->getDomainList() != false) {
return true;
}
return false;
}
//获取域名列表
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20)
{
$param = ['Action' => 'ListSites', 'SiteName' => $KeyWord, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'AccessType' => 'NS'];
$data = $this->request($param, 'GET', true);
if ($data) {
$list = [];
foreach ($data['Sites'] as $row) {
$list[] = [
'DomainId' => $row['SiteId'],
'Domain' => $row['SiteName'],
'RecordCount' => 0,
];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取解析记录列表
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$param = ['Action' => 'ListRecords', 'SiteId' => $this->domainid, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize];
if (!isNullOrEmpty($SubDomain)) {
$RecordName = $SubDomain == '@' ? $this->domain : $SubDomain . '.' . $this->domain;
$param += ['RecordName' => $RecordName];
} elseif (!isNullOrEmpty($KeyWord)) {
$RecordName = $KeyWord == '@' ? $this->domain : $KeyWord . '.' . $this->domain;
$param += ['RecordName' => $RecordName];
}
if (!isNullOrEmpty($Type)) {
if ($Type == 'A' || $Type == 'AAAA') $Type = 'A/AAAA';
$param += ['Type' => $Type];
}
if (!isNullOrEmpty($Line)) {
$param += ['Proxied' => $Line == '1' ? 'true' : 'false'];
}
$data = $this->request($param, 'GET', true);
if ($data) {
$list = [];
foreach ($data['Records'] as $row) {
$name = substr($row['RecordName'], 0, - (strlen($this->domain) + 1));
if ($name == '') $name = '@';
$value = $row['Data']['Value'];
if ($row['RecordType'] == 'CAA') $value = $row['Data']['Flag'] . ' ' . $row['Data']['Tag'] . ' ' . $row['Data']['Value'];
else if ($row['RecordType'] == 'SRV') $value = $row['Data']['Priority'] . ' ' . $row['Data']['Weight'] . ' ' . $row['Data']['Port'] . ' ' . $row['Data']['Value'];
if ($row['RecordType'] == 'A/AAAA') {
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$row['RecordType'] = 'A';
} elseif (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$row['RecordType'] = 'AAAA';
}
}
$list[] = [
'RecordId' => $row['RecordId'],
'Domain' => $this->domain,
'Name' => $name,
'Type' => $row['RecordType'],
'Value' => $value,
'Line' => $row['Proxied'] ? '1' : '0',
'TTL' => $row['Ttl'],
'MX' => isset($row['Data']['Priority']) ? $row['Data']['Priority'] : null,
'Status' => '1',
'Weight' => null,
'Remark' => isset($row['Comment']) ? $row['Comment'] : null,
'UpdateTime' => isset($row['UpdateTime']) ? date('Y-m-d H:i:s', strtotime($row['UpdateTime'])) : null,
];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
{
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId)
{
$param = ['Action' => 'GetRecord', 'RecordId' => $RecordId];
$data = $this->request($param, 'GET', true);
if ($data) {
$row = $data['RecordModel'];
$name = substr($row['RecordName'], 0, - (strlen($this->domain) + 1));
if ($name == '') $name = '@';
$value = $row['Data']['Value'];
if ($row['RecordType'] == 'CAA') $value = $row['Data']['Flag'] . ' ' . $row['Data']['Tag'] . ' ' . $row['Data']['Value'];
else if ($row['RecordType'] == 'SRV') $value = $row['Data']['Priority'] . ' ' . $row['Data']['Weight'] . ' ' . $row['Data']['Port'] . ' ' . $row['Data']['Value'];
if ($row['RecordType'] == 'A/AAAA') {
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$row['RecordType'] = 'A';
} elseif (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$row['RecordType'] = 'AAAA';
}
}
return [
'RecordId' => $row['RecordId'],
'Domain' => $this->domain,
'Name' => $name,
'Type' => $row['RecordType'],
'Value' => $value,
'Line' => $row['Proxied'] ? '1' : '0',
'TTL' => $row['Ttl'],
'MX' => isset($row['Data']['Priority']) ? $row['Data']['Priority'] : null,
'Status' => '1',
'Weight' => null,
'Remark' => isset($row['Comment']) ? $row['Comment'] : null,
'UpdateTime' => isset($row['UpdateTime']) ? date('Y-m-d H:i:s', strtotime($row['UpdateTime'])) : null,
];
}
return false;
}
//添加解析记录
public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = null, $Weight = null, $Remark = null)
{
if ($Name == '@') {
$Name = $this->domain;
} else {
$Name = $Name . '.' . $this->domain;
}
if ($Type == 'A' || $Type == 'AAAA') $Type = 'A/AAAA';
$data = ['Value' => $Value];
if ($Type == 'CAA') {
list($flag, $tag, $val) = explode(' ', $Value, 3);
$data = ['Flag' => intval($flag), 'Tag' => $tag, 'Value' => $val];
} elseif ($Type == 'SRV') {
list($priority, $weight, $port, $val) = explode(' ', $Value, 4);
$data = ['Priority' => intval($priority), 'Weight' => intval($weight), 'Port' => intval($port), 'Value' => $val];
} elseif ($Type == 'MX') {
$data['Priority'] = intval($MX);
}
$param = ['Action' => 'CreateRecord', 'SiteId' => $this->domainid, 'RecordName' => $Name, 'Type' => $Type, 'Proxied' => $Line == '1' ? 'true' : 'false', 'Ttl' => intval($TTL), 'Data' => json_encode($data), 'Comment' => $Remark];
if ($Line == '1') $param['BizName'] = 'web';
$data = $this->request($param, 'POST', true);
if ($data) {
return $data['RecordId'];
}
return false;
}
//修改解析记录
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = null, $Weight = null, $Remark = null)
{
if ($Name == '@') {
$Name = $this->domain;
} else {
$Name = $Name . '.' . $this->domain;
}
if ($Type == 'A' || $Type == 'AAAA') $Type = 'A/AAAA';
$data = ['Value' => $Value];
if ($Type == 'CAA') {
list($flag, $tag, $val) = explode(' ', $Value, 3);
$data = ['Flag' => intval($flag), 'Tag' => $tag, 'Value' => $val];
} elseif ($Type == 'SRV') {
list($priority, $weight, $port, $val) = explode(' ', $Value, 4);
$data = ['Priority' => intval($priority), 'Weight' => intval($weight), 'Port' => intval($port), 'Value' => $val];
} elseif ($Type == 'MX') {
$data['Priority'] = intval($MX);
}
$param = ['Action' => 'UpdateRecord', 'RecordId' => $RecordId, 'Type' => $Type, 'Proxied' => $Line == '1' ? 'true' : 'false', 'Ttl' => intval($TTL), 'Data' => json_encode($data), 'Comment' => $Remark];
if ($Line == '1') $param['BizName'] = 'web';
return $this->request($param, 'POST');
}
//修改解析记录备注
public function updateDomainRecordRemark($RecordId, $Remark)
{
return false;
}
//删除解析记录
public function deleteDomainRecord($RecordId)
{
$param = ['Action' => 'DeleteRecord', 'RecordId' => $RecordId];
return $this->request($param, 'POST');
}
//设置解析记录状态
public function setDomainRecordStatus($RecordId, $Status)
{
return false;
}
//获取解析记录操作日志
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
{
return false;
}
//获取解析线路列表
public function getRecordLine()
{
return ['0' => ['name' => '仅DNS', 'parent' => null], '1' => ['name' => '已代理', 'parent' => null]];
}
//获取域名信息
public function getDomainInfo()
{
$param = ['Action' => 'GetSite', 'SiteId' => $this->domainid];
$data = $this->request($param, 'GET', true);
if ($data) {
return $data;
}
return false;
}
//获取域名最低TTL
public function getMinTTL()
{
return 1;
}
public function addDomain($Domain)
{
return false;
}
private function request($param, $method, $returnData = false)
{
if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) return false;
try {
$result = $this->client->request($param, $method);
} catch (Exception $e) {
$this->setError($e->getMessage());
return false;
}
return $returnData ? $result : true;
}
private function setError($message)
{
$this->error = $message;
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
}
}

View File

@ -18,8 +18,8 @@ class baidu implements DnsInterface
public function __construct($config)
{
$this->AccessKeyId = $config['ak'];
$this->SecretAccessKey = $config['sk'];
$this->AccessKeyId = $config['AccessKeyId'];
$this->SecretAccessKey = $config['SecretAccessKey'];
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $proxy);
$this->domain = $config['domain'];

276
app/lib/dns/bt.php Normal file
View File

@ -0,0 +1,276 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
class bt implements DnsInterface
{
private $accountId;
private $accessKey;
private $secretKey;
private $baseUrl = 'https://dmp.bt.cn';
private $error;
private $domain;
private $domainid;
private $domainType;
private $proxy;
public function __construct($config)
{
$this->accountId = $config['AccountID'];
$this->accessKey = $config['AccessKey'];
$this->secretKey = $config['SecretKey'];
$this->domain = $config['domain'];
if ($config['domainid']) {
$a = explode('|', $config['domainid']);
$this->domainid = intval($a[0]);
$this->domainType = isset($a[1]) ? intval($a[1]) : 1;
}
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function getError()
{
return $this->error;
}
public function check()
{
if ($this->getDomainList() != false) {
return true;
}
return false;
}
//获取域名列表
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20)
{
$param = ['p' => $PageNumber, 'rows' => $PageSize, 'keyword' => $KeyWord];
$data = $this->execute('/api/v1/dns/manage/list_domains', $param);
if ($data) {
$list = [];
foreach ($data['data'] as $row) {
$list[] = [
'DomainId' => $row['local_id'] . '|' . $row['domain_type'],
'Domain' => $row['full_domain'],
'RecordCount' => $row['record_count'],
];
}
return ['total' => $data['total'], 'list' => $list];
}
return false;
}
//获取解析记录列表
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$param = ['domain_id' => $this->domainid, 'domain_type' => $this->domainType, 'p' => $PageNumber, 'rows' => $PageSize];
if (!isNullOrEmpty($SubDomain)) {
$param['searchKey'] = 'record';
$param['searchValue'] = $SubDomain;
} elseif (!isNullOrEmpty($KeyWord)) {
$param['searchKey'] = 'record';
$param['searchValue'] = $KeyWord;
} elseif (!isNullOrEmpty($Value)) {
$param['searchKey'] = 'value';
$param['searchValue'] = $Value;
} elseif (!isNullOrEmpty($Type)) {
$param['searchKey'] = 'type';
$param['searchValue'] = $Type;
} elseif (!isNullOrEmpty($Status)) {
$param['searchKey'] = 'state';
$param['searchValue'] = $Status == '0' ? '1' : '0';
} elseif (!isNullOrEmpty($Line)) {
$param['searchKey'] = 'line';
$param['searchValue'] = $Line;
}
$data = $this->execute('/api/v1/dns/record/list', $param);
if ($data) {
$list = [];
foreach ($data['data'] as $row) {
$list[] = [
'RecordId' => $row['record_id'],
'Domain' => $this->domain,
'Name' => $row['record'],
'Type' => $row['type'],
'Value' => $row['value'],
'Line' => $row['viewID'],
'TTL' => $row['TTL'],
'MX' => $row['MX'],
'Status' => $row['state'] == 1 ? '0' : '1',
'Weight' => $row['MX'],
'Remark' => $row['remark'],
'UpdateTime' => date('Y-m-d H:i:s', strtotime($row['created_at'])),
];
}
return ['total' => $data['count'], 'list' => $list];
}
return false;
}
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
{
if ($SubDomain == '') $SubDomain = '@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId)
{
return false;
}
//添加解析记录
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$param = ['domain_id' => $this->domainid, 'domain_type' => $this->domainType, 'type' => $Type, 'record' => $Name, 'value' => $Value, 'ttl' => intval($TTL), 'view_id' => intval($Line), 'remark' => $Remark];
if (!$Weight) $Weight = 1;
if ($Type == 'MX') $param['mx'] = intval($MX);
else $param['mx'] = intval($Weight);
$data = $this->execute('/api/v1/dns/record/create', $param);
return $data !== false;
}
//修改解析记录
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$param = ['record_id' => $RecordId, 'domain_id' => $this->domainid, 'domain_type' => $this->domainType, 'type' => $Type, 'record' => $Name, 'value' => $Value, 'ttl' => intval($TTL), 'view_id' => intval($Line), 'remark' => $Remark];
if (!$Weight) $Weight = 1;
if ($Type == 'MX') $param['mx'] = intval($MX);
else $param['mx'] = intval($Weight);
$data = $this->execute('/api/v1/dns/record/update', $param);
return $data !== false;
}
//修改解析记录备注
public function updateDomainRecordRemark($RecordId, $Remark)
{
return false;
}
//删除解析记录
public function deleteDomainRecord($RecordId)
{
$param = ['id' => $RecordId, 'domain_id' => $this->domainid, 'domain_type' => $this->domainType];
$data = $this->execute('/api/v1/dns/record/delete', $param);
return $data !== false;
}
//设置解析记录状态
public function setDomainRecordStatus($RecordId, $Status)
{
$param = ['record_id' => $RecordId, 'domain_id' => $this->domainid, 'domain_type' => $this->domainType];
$data = $this->execute($Status == '0' ? '/api/v1/dns/record/pause' : '/api/v1/dns/record/start', $param);
return $data !== false;
}
//获取解析记录操作日志
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
{
return false;
}
//获取解析线路列表
public function getRecordLine()
{
$param = [];
$data = $this->execute('/api/v1/dns/record/get_views', $param);
if ($data) {
$list = [];
$this->processLineList($list, $data, null);
return $list;
}
return false;
}
private function processLineList(&$list, $line_list, $parent)
{
foreach ($line_list as $row) {
if ($row['free'] && !isset($list[$row['viewId']])) {
$list[$row['viewId']] = ['name' => $row['name'], 'parent' => $parent];
if ($row['children']) {
$this->processLineList($list, $row['children'], $row['viewId']);
}
}
}
}
//获取域名信息
public function getDomainInfo()
{
return false;
}
//获取域名最低TTL
public function getMinTTL()
{
return 300;
}
public function addDomain($Domain)
{
$param = ['full_domain' => $Domain];
$data = $this->execute('/api/v1/dns/manage/add_external_domain', $param);
if ($data) {
return ['id' => $data['domain_id'], 'name' => $data['full_domain']];
}
return false;
}
private function execute($path, $params)
{
$method = 'POST';
$timestamp = (string)time();
$body = json_encode($params);
$signingString = implode("\n", [
$this->accountId,
$timestamp,
$method,
$path,
$body
]);
$signature = hash_hmac('sha256', $signingString, $this->secretKey);
$headers = [
'Content-Type' => 'application/json',
'X-Account-ID' => $this->accountId,
'X-Access-Key' => $this->accessKey,
'X-Timestamp' => $timestamp,
'X-Signature' => $signature
];
$response = $this->curl($method, $path, $headers, $body);
if (!$response) {
return false;
}
$arr = json_decode($response, true);
if ($arr) {
if ($arr['code'] == 0) {
return $arr['data'];
} else {
$this->setError($arr['msg']);
return false;
}
} else {
$this->setError('返回数据解析失败');
return false;
}
}
private function curl($method, $path, $header, $body = null)
{
$url = $this->baseUrl . $path;
try {
$response = http_request($url, $body, null, null, $header, $this->proxy, $method);
} catch (\Exception $e) {
$this->setError($e->getMessage());
return false;
}
return $response['body'];
}
private function setError($message)
{
$this->error = $message;
}
}

View File

@ -8,6 +8,7 @@ class cloudflare implements DnsInterface
{
private $Email;
private $ApiKey;
private $auth;
private $baseUrl = 'https://api.cloudflare.com/client/v4';
private $error;
private $domain;
@ -16,11 +17,12 @@ class cloudflare implements DnsInterface
function __construct($config)
{
$this->Email = $config['ak'];
$this->ApiKey = $config['sk'];
$this->Email = $config['email'];
$this->ApiKey = $config['apikey'];
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->auth = isset($config['auth']) ? intval($config['auth']) : (preg_match('/^[0-9a-f]+$/i', $this->ApiKey) ? 0 : 1);
}
public function getError()
@ -75,8 +77,9 @@ class cloudflare implements DnsInterface
if ($data) {
$list = [];
foreach ($data['result'] as $row) {
$name = $this->domain == $row['name'] ? '@' : str_replace('.'.$this->domain, '', $row['name']);
$name = $this->domain == $row['name'] ? '@' : substr($row['name'], 0, -(strlen($this->domain) + 1));
$status = str_ends_with($name, '_pause') ? '0' : '1';
$name = $name == '__root__' ? '@' : $name;
$name = $status == '0' ? substr($name, 0, -6) : $name;
if ($row['type'] == 'SRV' && isset($row['priority'])) {
$row['content'] = $row['priority'] . ' ' . $row['content'];
@ -112,9 +115,10 @@ class cloudflare implements DnsInterface
{
$data = $this->send_reuqest('GET', '/zones/'.$this->domainid.'/dns_records/'.$RecordId);
if ($data) {
$name = $this->domain == $data['result']['name'] ? '@' : str_replace('.' . $this->domain, '', $data['result']['name']);
$name = $this->domain == $data['result']['name'] ? '@' : substr($data['result']['name'], 0, -(strlen($this->domain) + 1));
$status = str_ends_with($name, '_pause') ? '0' : '1';
$name = $status == '0' ? substr($name, 0, -6) : $name;
$name = $name == '__root__' ? '@' : $name;
if ($data['result']['type'] == 'SRV' && isset($data['result']['priority'])) {
$data['result']['content'] = $data['result']['priority'] . ' ' . $data['result']['content'];
}
@ -180,6 +184,12 @@ class cloudflare implements DnsInterface
{
$info = $this->getDomainRecordInfo($RecordId);
$Name = $Status == '1' ? str_replace('_pause', '', $info['Name']) : $info['Name'] . '_pause';
// @ 作为特殊字符不能设置为解析, 故设置暂停解析的时候, 替换为 __root__
if ($Name == '__root__') {
$Name = '@';
} elseif ($Name == '@_pause') {
$Name = '__root___pause';
}
return $this->updateDomainRecord($RecordId, $Name, $info['Type'], $info['Value'], $info['Line'], $info['TTL'], $info['MX'], $info['Weight'], $info['Remark']);
}
@ -263,14 +273,14 @@ class cloudflare implements DnsInterface
{
$url = $this->baseUrl . $path;
if (preg_match('/^[0-9a-f]+$/i', $this->ApiKey)) {
if ($this->auth == 0) {
$headers = [
'X-Auth-Email: ' . $this->Email,
'X-Auth-Key: ' . $this->ApiKey,
'X-Auth-Email' => $this->Email,
'X-Auth-Key' => $this->ApiKey,
];
} else {
$headers = [
'Authorization: Bearer ' . $this->ApiKey,
'Authorization' => 'Bearer ' . $this->ApiKey,
];
}
@ -281,39 +291,17 @@ class cloudflare implements DnsInterface
}
} else {
$body = json_encode($params);
$headers[] = 'Content-Type: application/json';
$headers['Content-Type'] = 'application/json';
}
$ch = curl_init($url);
if ($this->proxy) {
curl_set_proxy($ch);
try {
$response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
} catch (\Exception $e) {
$this->setError($e->getMessage());
return false;
}
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
if ($method == 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'PATCH') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$this->setError('Curl error: ' . curl_error($ch));
}
curl_close($ch);
if ($errno) return false;
$arr = json_decode($response, true);
$arr = json_decode($response['body'], true);
if ($arr) {
if ($arr['success']) {
return $arr;

View File

@ -17,8 +17,8 @@ class dnsla implements DnsInterface
public function __construct($config)
{
$this->apiid = $config['ak'];
$this->apisecret = $config['sk'];
$this->apiid = $config['apiid'];
$this->apisecret = $config['apisecret'];
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
@ -60,19 +60,19 @@ class dnsla implements DnsInterface
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$param = ['domainId' => $this->domainid, 'pageIndex' => $PageNumber, 'pageSize' => $PageSize];
if (!isNullOrEmpty(($KeyWord))) {
if (!isNullOrEmpty($KeyWord)) {
$param['host'] = $KeyWord;
}
if (!isNullOrEmpty(($Type))) {
if (!isNullOrEmpty($Type)) {
$param['type'] = $this->convertType($Type);
}
if (!isNullOrEmpty(($Line))) {
if (!isNullOrEmpty($Line)) {
$param['lineId'] = $Line;
}
if (!isNullOrEmpty(($SubDomain))) {
if (!isNullOrEmpty($SubDomain)) {
$param['host'] = $SubDomain;
}
if (!isNullOrEmpty(($Value))) {
if (!isNullOrEmpty($Value)) {
$param['data'] = $Value;
}
$data = $this->execute('GET', '/api/recordList', $param);
@ -235,7 +235,10 @@ class dnsla implements DnsInterface
private function execute($method, $path, $params = null)
{
$token = base64_encode($this->apiid.':'.$this->apisecret);
$header = ['Authorization: Basic '.$token, 'Content-Type: application/json; charset=utf-8'];
$header = [
'Authorization' => 'Basic '.$token,
'Content-Type' => 'application/json; charset=utf-8'
];
if ($method == 'POST' || $method == 'PUT') {
$response = $this->curl($method, $path, $header, json_encode($params));
} else {
@ -264,34 +267,19 @@ class dnsla implements DnsInterface
private function curl($method, $path, $header, $body = null)
{
$url = $this->baseUrl . $path;
$ch = curl_init($url);
if ($this->proxy) {
curl_set_proxy($ch);
try {
$response = http_request($url, $body, null, null, $header, $this->proxy, $method);
} catch (\Exception $e) {
$this->setError($e->getMessage());
return false;
}
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($body) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$this->setError('Curl error: ' . curl_error($ch));
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($errno) return false;
if ($httpCode == 200) {
return $response;
} elseif ($httpCode == 401) {
if ($response['code'] == 200) {
return $response['body'];
} elseif ($response['code'] == 401) {
$this->setError('认证失败');
return false;
} else {
$this->setError('http code: '.$httpCode);
$this->setError('http code: '.$response['code']);
return false;
}
}

View File

@ -21,8 +21,8 @@ class dnspod implements DnsInterface
public function __construct($config)
{
$this->SecretId = $config['ak'];
$this->SecretKey = $config['sk'];
$this->SecretId = $config['SecretId'];
$this->SecretKey = $config['SecretKey'];
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new TencentCloud($this->SecretId, $this->SecretKey, $this->endpoint, $this->service, $this->version, null, $proxy);
$this->domain = $config['domain'];

View File

@ -18,8 +18,8 @@ class huawei implements DnsInterface
public function __construct($config)
{
$this->AccessKeyId = $config['ak'];
$this->SecretAccessKey = $config['sk'];
$this->AccessKeyId = $config['AccessKeyId'];
$this->SecretAccessKey = $config['SecretAccessKey'];
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $proxy);
$this->domain = $config['domain'];
@ -77,12 +77,13 @@ class huawei implements DnsInterface
if ($data) {
$list = [];
foreach ($data['recordsets'] as $row) {
if ($row['name'] == $row['zone_name']) $row['name'] = '@';
$name = substr($row['name'], 0, -(strlen($row['zone_name']) + 1));
if ($name == '') $name = '@';
if ($row['type'] == 'MX') list($row['mx'], $row['records']) = explode(' ', $row['records'][0]);
$list[] = [
'RecordId' => $row['id'],
'Domain' => rtrim($row['zone_name'], '.'),
'Name' => str_replace('.'.$row['zone_name'], '', $row['name']),
'Name' => $name,
'Type' => $row['type'],
'Value' => $row['records'],
'Line' => $row['line'],
@ -110,12 +111,13 @@ class huawei implements DnsInterface
{
$data = $this->send_request('GET', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId);
if ($data) {
if ($data['name'] == $data['zone_name']) $data['name'] = '@';
$name = substr($data['name'], 0, -(strlen($data['zone_name']) + 1));
if ($name == '') $name = '@';
if ($data['type'] == 'MX') list($data['mx'], $data['records']) = explode(' ', $data['records'][0]);
return [
'RecordId' => $data['id'],
'Domain' => rtrim($data['zone_name'], '.'),
'Name' => str_replace('.'.$data['zone_name'], '', $data['name']),
'Name' => $name,
'Type' => $data['type'],
'Value' => $data['records'],
'Line' => $data['line'],

View File

@ -30,8 +30,8 @@ class huoshan implements DnsInterface
public function __construct($config)
{
$this->AccessKeyId = $config['ak'];
$this->SecretAccessKey = $config['sk'];
$this->AccessKeyId = $config['AccessKeyId'];
$this->SecretAccessKey = $config['SecretAccessKey'];
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $this->service, $this->version, $this->region, $proxy);
$this->domain = $config['domain'];

View File

@ -23,8 +23,8 @@ class jdcloud implements DnsInterface
public function __construct($config)
{
$this->AccessKeyId = $config['ak'];
$this->AccessKeySecret = $config['sk'];
$this->AccessKeyId = $config['AccessKeyId'];
$this->AccessKeySecret = $config['AccessKeySecret'];
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new JdcloudClient($this->AccessKeyId, $this->AccessKeySecret, $this->endpoint, $this->service, $this->region, $proxy);
$this->domain = $config['domain'];

View File

@ -16,7 +16,7 @@ class namesilo implements DnsInterface
function __construct($config)
{
$this->apikey = $config['sk'];
$this->apikey = $config['apikey'];
$this->domain = $config['domain'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
@ -63,11 +63,10 @@ class namesilo implements DnsInterface
if ($data) {
$list = [];
foreach ($data['resource_record'] as $row) {
$name = $row['host'] == $this->domain ? '@' : str_replace('.'.$this->domain, '', $row['host']);
$list[] = [
'RecordId' => $row['record_id'],
'Domain' => $this->domain,
'Name' => $name,
'Name' => $row['host'],
'Type' => $row['type'],
'Value' => $row['value'],
'Line' => 'default',

View File

@ -17,8 +17,8 @@ class powerdns implements DnsInterface
function __construct($config)
{
$this->url = 'http://' . $config['ak'] . ':' . $config['sk'] . '/api/v1';
$this->apikey = $config['ext'];
$this->url = 'http://' . $config['ip'] . ':' . $config['port'] . '/api/v1';
$this->apikey = $config['apikey'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];

387
app/lib/dns/qingcloud.php Normal file
View File

@ -0,0 +1,387 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
class qingcloud implements DnsInterface
{
private $access_key_id;
private $secret_access_key;
private $baseUrl = 'http://api.routewize.com';
private $error;
private $domain;
private $domainid;
private $proxy;
public function __construct($config)
{
$this->access_key_id = $config['access_key_id'];
$this->secret_access_key = $config['secret_access_key'];
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function getError()
{
return $this->error;
}
public function check()
{
if ($this->getDomainList() != false) {
return true;
}
return false;
}
//获取域名列表
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20)
{
$offset = ($PageNumber - 1) * $PageSize;
$param = ['offset' => $offset, 'limit' => $PageSize];
if (!empty($KeyWord)) {
$param['zone_name'] = $KeyWord;
}
$data = $this->execute('GET', '/v1/user/zones', $param);
if ($data) {
$list = [];
foreach ($data['zones'] as $row) {
$list[] = [
'DomainId' => $row['zone_name'],
'Domain' => rtrim($row['zone_name'], '.'),
'RecordCount' => 0,
];
}
return ['total' => $data['total_count'], 'list' => $list];
}
return false;
}
//获取解析记录列表
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
if ($SubDomain) {
return $this->getHostRecords($SubDomain);
}
$offset = ($PageNumber - 1) * $PageSize;
$param = ['zone_name' => $this->domainid, 'offset' => $offset, 'limit' => $PageSize];
if (!isNullOrEmpty($KeyWord)) {
$param['search_word'] = $KeyWord;
}
$data = $this->execute('GET', '/v1/dns/host/', $param);
if ($data) {
$list = [];
foreach ($data['domains'] as $row) {
$name = substr($row['domain_name'], 0, -(strlen($row['zone_name']) + 1));
if ($name == '') $name = '@';
$list[] = [
'RecordId' => $row['domain_name'],
'Domain' => $this->domain,
'Name' => $name,
'Type' => null,
'Value' => null,
'Line' => null,
'TTL' => null,
'MX' => null,
'Status' => $row['status'] == 'enabled' ? '0' : '1',
'Weight' => null,
'Remark' => $row['description'],
'UpdateTime' => $row['create_time'],
'Count' => $row['count'],
];
}
return ['total' => $data['total_count'], 'list' => $list];
}
return false;
}
private function getHostRecords($SubDomain)
{
$param = ['zone_name' => $this->domainid, 'domain_name' => $SubDomain];
$data = $this->execute('GET', '/v1/dns/host_info/', $param);
if ($data) {
$list = [];
foreach ($data['records'] as $record) {
$name = substr($record['domain_name'], 0, -(strlen($record['zone_name']) + 1));
if ($name == '') $name = '@';
foreach ($record['record'] as $record_group) {
foreach ($record_group['data'] as $row) {
$mx = null;
if ($record['rd_type'] == 'MX') {
$value = explode(' ', $row['value'], 2);
$row['value'] = isset($value[1]) ? $value[1] : '';
$mx = intval($value[0]);
}
if ($record['rd_type'] == 'TXT') {
$row['value'] = trim($row['value'], '"');
}
$list[] = [
'RecordId' => $record['domain_record_id'].'_'.$row['record_value_id'],
'Domain' => $record['domain_name'],
'Name' => $name,
'Type' => $record['rd_type'],
'Mode' => $record['mode'],
'Value' => $row['value'],
'Line' => $record['view_id'],
'TTL' => $record['ttl'],
'MX' => $mx,
'Status' => $row['status'] == 1 ? '1' : '0',
'Weight' => $record_group['weight'] > 0 ? $record_group['weight'] : null,
'Remark' => null,
'UpdateTime' => $record['create_time'],
];
}
}
}
return ['total' => $data['total_count'], 'list' => $list];
}
return false;
}
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
{
$SubDomain = $this->getHost($SubDomain);
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId)
{
return false;
}
//添加解析记录
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$mode = input('post.mode', '1');
if ($Type == 'MX') {
$Value = intval($MX).' '.$Value;
} elseif ($Type == 'TXT' && substr($Value, 0, 1) != '"') {
$Value = '"'.$Value.'"';
}
$values = [];
foreach (explode(',', $Value) as $val) {
$values[] = ['value' => trim($val), 'status' => 1];
}
if (($Type == 'A' || $Type == 'CNAME') && $mode == '3') $Weight = intval($Weight);
else $Weight = 0;
$record = [['weight' => $Weight, 'values' => $values]];
$param = ['zone_name' => $this->domainid, 'domain_name' => $Name, 'view_id' => intval($Line), 'type' => $Type, 'ttl' => intval($TTL), 'record' => json_encode($record), 'mode' => intval($mode), 'auto_merge' => 2];
$data = $this->execute('POST', '/v1/record/', $param);
return is_array($data) ? $data['domain_record_id'] : false;
}
//修改解析记录
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$mode = input('post.mode', '1');
if ($Type == 'MX') {
$Value = intval($MX).' '.$Value;
} elseif ($Type == 'TXT' && substr($Value, 0, 1) != '"') {
$Value = '"'.$Value.'"';
}
$recordId = explode('_', $RecordId);
$domain_record_id = $recordId[0];
$record_value_id = $recordId[1];
$data = $this->execute('GET', '/v1/dr_id/'.$domain_record_id);
if (!$data) return false;
if (($Type == 'A' || $Type == 'CNAME') && $mode == '3') $Weight = intval($Weight);
else $Weight = 0;
$record = [];
foreach ($data['data']['record'] as $record_group) {
$values = [];
$flag = false;
foreach ($record_group['data'] as $row) {
if ($row['record_value_id'] == $record_value_id) {
$row['value'] = $Value;
$flag = true;
}
$values[] = ['value' => $row['value'], 'status' => $row['status']];
}
if (count($values) > 0) {
$record[] = ['weight' => $flag ? $Weight : $record_group['weight'], 'values' => $values];
}
}
$param = ['zone_name' => $this->domainid, 'domain_name' => $Name, 'view_id' => intval($Line), 'type' => $Type, 'ttl' => intval($TTL), 'record' => json_encode($record), 'mode' => intval($mode)];
$data = $this->execute('POST', '/v1/dr_id/'.$domain_record_id, $param);
return $data !== false;
}
//修改解析记录备注
public function updateDomainRecordRemark($RecordId, $Remark)
{
$param = ['zone_name' => $this->domainid, 'domain_name' => $RecordId, 'description' => $Remark];
$data = $this->execute('POST', '/v1/dns/host/', $param);
return $data !== false;
}
//删除解析记录
public function deleteDomainRecord($RecordId)
{
if (strpos($RecordId, $this->domainid) !== false) {
$param = ['domain_names' => json_encode([$RecordId]), 'zone_name' => $this->domainid];
$data = $this->execute('DELETE', '/v1/domain/', $param);
return $data !== false;
}
$recordId = explode('_', $RecordId);
$domain_record_id = $recordId[0];
$record_value_id = $recordId[1];
$data = $this->execute('GET', '/v1/dr_id/'.$domain_record_id);
if (!$data) return false;
$record = [];
foreach ($data['data']['record'] as $record_group) {
$values = [];
foreach ($record_group['data'] as $row) {
if ($row['record_value_id'] == $record_value_id) {
continue;
}
$values[] = ['value' => $row['value'], 'status' => $row['status']];
}
if (count($values) > 0) {
$record[] = ['weight' => $record_group['weight'], 'values' => $values];
}
}
if (count($record) == 0) {
$param = ['ids' => json_encode([$domain_record_id]), 'target' => 'record', 'action' => 'delete'];
$data = $this->execute('POST', '/v1/change_record_status/', $param);
return $data !== false;
}
$name = substr($data['data']['domain_name'], 0, -(strlen($data['data']['zone_name']) + 1));
if ($name == '') $name = '@';
$param = ['zone_name' => $this->domainid, 'domain_name' => $name, 'view_id' => $data['data']['view_id'], 'type' => $data['data']['rd_type'], 'ttl' => $data['data']['ttl'], 'record' => json_encode($record), 'mode' => $data['data']['mode']];
$data = $this->execute('POST', '/v1/dr_id/'.$domain_record_id, $param);
return $data !== false;
}
//设置解析记录状态
public function setDomainRecordStatus($RecordId, $Status)
{
$recordId = explode('_', $RecordId);
$record_value_id = $recordId[1];
$param = ['ids' => json_encode([$record_value_id]), 'target' => 'value', 'action' => $Status == '0' ? 'stop' : 'enable'];
$data = $this->execute('POST', '/v1/change_record_status/', $param);
return $data !== false;
}
//获取解析记录操作日志
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
{
return false;
}
//获取解析线路列表
public function getRecordLine()
{
$param = ['zone_name' => $this->domainid, 'type' => 'GET_FULL'];
$data = $this->execute('GET', '/v1/zone/view/', $param);
if ($data) {
$list = [];
foreach ($data['zone_views'] as $row) {
if ($row['name'] == '*') $row['name'] = '默认';
$list[$row['id']] = ['name' => $row['name'], 'parent' => null];
}
return $list;
}
return false;
}
//获取域名信息
public function getDomainInfo()
{
return false;
}
//获取域名最低TTL
public function getMinTTL()
{
return 60;
}
public function addDomain($Domain)
{
$param = ['zone_name' => $Domain];
$data = $this->execute('POST', '/v1/zone/', $param);
if ($data) {
return ['id' => $data['zone_name'], 'name' => $Domain];
}
return false;
}
private function getHost($Name)
{
if ($Name == '@' || $Name == '') $Name = '';
else $Name .= '.';
$Name .= $this->domain . '.';
return $Name;
}
private function execute($method, $path, $params = null)
{
$date = gmdate('D, d M Y H:i:s \G\M\T');
$string_to_sign = $method."\n".$date."\n".$path;
if($method == 'GET' && $params){
ksort($params);
$string_to_sign .= '?'.http_build_query($params);
}
$signature = base64_encode(hash_hmac('sha256', $string_to_sign, $this->secret_access_key, true));
$authorization = 'QC-HMAC-SHA256 '.$this->access_key_id.':'.$signature;
$header = [
'Authorization' => $authorization,
'Date' => $date,
];
if ($method == 'POST' || $method == 'PUT' || $method == 'DELETE') {
$header['Content-Type'] = 'application/json; charset=utf-8';
$response = $this->curl($method, $path, $header, json_encode($params));
} else {
if ($params) {
$path .= '?'.http_build_query($params);
}
$response = $this->curl($method, $path, $header);
}
$arr = json_decode($response['body'], true);
if (isset($arr['code']) && $arr['code'] == 0 || isset($arr['domains']) || $method == 'DELETE' && $response['code'] == 204) {
return $arr;
} elseif(isset($arr['message'])) {
$this->setError($arr['message']);
return false;
} elseif(isset($arr['msg'])) {
$this->setError($arr['msg']);
return false;
} else {
$this->setError('返回数据解析失败');
return false;
}
}
private function curl($method, $path, $header, $body = null)
{
$url = $this->baseUrl . $path;
try {
$response = http_request($url, $body, null, null, $header, $this->proxy, $method);
} catch (\Exception $e) {
$this->setError($e->getMessage());
return false;
}
return $response;
}
private function setError($message)
{
$this->error = $message;
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
}
}

View File

@ -18,8 +18,8 @@ class spaceship implements DnsInterface
public function __construct($config)
{
$this->apiKey = $config['ak'];
$this->apiSecret = $config['sk'];
$this->apiKey = $config['apikey'];
$this->apiSecret = $config['apisecret'];
$this->domain = $config['domain'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
@ -57,90 +57,12 @@ class spaceship implements DnsInterface
}
//获取解析记录列表
private function send_reuqest($method, $path, $params = null)
{
$url = $this->baseUrl . $path;
$headers = [
'X-API-Key: ' . $this->apiKey,
'X-API-Secret: ' . $this->apiSecret,
];
$body = '';
if ($method == 'GET') {
if ($params) {
$url .= '?' . http_build_query($params);
}
} else {
$body = json_encode($params);
$headers[] = 'Content-Type: application/json';
}
$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, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
if ($method == 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'PATCH') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$this->setError('Curl error: ' . curl_error($ch));
}
curl_close($ch);
if ($errno) return false;
$arr = json_decode($response, true);
if (!isset($arr['detail'])) {
return $arr;
} else {
$this->setError($response['detail']);
return false;
}
}
//获取子域名解析记录列表
private function setError($message)
{
$this->error = $message;
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
}
//获取解析记录详细信息
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
{
if ($SubDomain == '') $SubDomain = '@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
}
//添加解析记录
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$param = ['take' => $PageSize, 'skip' => ($PageNumber - 1) * $PageSize];
if (!isNullOrEmpty(($SubDomain))) {
$param['host'] = $SubDomain;
if (!isNullOrEmpty($SubDomain)) {
$param['take'] = 100;
$param['skip'] = 0;
}
$data = $this->send_reuqest('GET', '/dns/records/' . $this->domain, $param);
if ($data) {
@ -148,39 +70,26 @@ class spaceship implements DnsInterface
foreach ($data['items'] as $row) {
$type = $row['type'];
$name = $row['name'];
$mx = 0;
if ('MX' == $type) {
$address = $row['exchange'];
$mx = $row['preference'];
} else if ('CNAME' == $type) {
$address = $row['cname'];
$mx = 0;
} else if ('TXT' == $type) {
$address = $row['value'];
$mx = 0;
} else if ('PTR' == $type) {
$address = $row['pointer'];
$mx = 0;
} else if ('NS' == $type) {
$address = $row['nameserver'];
$mx = 0;
} else if ('HTTPS' == $type) {
$address = $row['targetName'] . $row['svcParams'] . '|' . $row['svcPriority'];
$mx = 0;
} else if ('CAA' == $type) {
$address = $row['value'];
$mx = 0;
} else if ('TLSA' == $type) {
$address = $row['associationData'];
$mx = 0;
} else if ('SVRB' == $type) {
$address = $row['targetName'] . $row['svcParams'] . '|' . $row['svcPriority'];
$mx = 0;
$address = $row['flag'] . ' ' . $row['tag'] . ' ' . $row['value'];
} else if ('SRV' == $type) {
$address = $row['priority'] . ' ' . $row['weight'] . ' ' . $row['port'] . ' ' . $row['target'];
} else if ('ALIAS' == $type) {
$address = $row['aliasName'];
$mx = 0;
} else {
$address = $row['address'];
$mx = 0;
}
$list[] = [
@ -198,72 +107,106 @@ class spaceship implements DnsInterface
'UpdateTime' => null,
];
}
if(!isNullOrEmpty($SubDomain)){
$list = array_values(array_filter($list, function($v) use ($SubDomain){
return strcasecmp($v['Name'], $SubDomain) === 0;
}));
}
return ['total' => $data['total'], 'list' => $list];
}
return false;
}
//修改解析记录
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
{
if ($SubDomain == '') $SubDomain = '@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId)
{
return false;
}
//修改解析记录备注
private function convertRecordItem($Name, $Type, $Value, $MX)
{
$item = [
'type' => $Type,
'name' => $Name,
];
if ($Type == 'MX') {
$item['exchange'] = $Value;
$item['preference'] = (int)$MX;
} else if ($Type == 'TXT') {
$item['value'] = $Value;
} else if ($Type == 'CNAME') {
$item['cname'] = $Value;
} else if ($Type == 'ALIAS') {
$item['aliasName'] = $Value;
} else if ($Type == 'NS') {
$item['nameserver'] = $Value;
} else if ($Type == 'PTR') {
$item['pointer'] = $Value;
} else if ($Type == 'CAA') {
$parts = explode(' ', $Value, 3);
if (count($parts) >= 3) {
$item['flag'] = (int)$parts[0];
$item['tag'] = $parts[1];
$item['value'] = trim($parts[2], '"');
}
} else if ($Type == 'SRV') {
$parts = explode(' ', $Value, 4);
if (count($parts) >= 4) {
$item['priority'] = (int)$parts[0];
$item['weight'] = (int)$parts[1];
$item['port'] = (int)$parts[2];
$item['target'] = $parts[3];
}
} else {
$item['address'] = $Value;
}
return $item;
}
//添加解析记录
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$item = $this->convertRecordItem($Name, $Type, $Value, $MX);
$item['ttl'] = (int)$TTL;
$param = [
'force' => true,
'force' => false,
'items' => [
[
'type' => $this->convertType($Type),
'name' => $Name,
'address' => $Value,
'ttl' => $TTL,
]
$item
]
];
$data = $this->send_reuqest('PUT', '/dns/records/' . $this->domain, $param);
return !isset($data);
}
//删除解析记录
private function convertType($type)
{
return $type;
}
//设置解析记录状态
//修改解析记录
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$item = $this->convertRecordItem($Name, $Type, $Value, $MX);
$item['ttl'] = (int)$TTL;
$param = [
'force' => true,
'items' => [
[
'type' => $this->convertType($Type),
'name' => $Name,
'address' => $Value,
'ttl' => $TTL,
]
$item
]
];
$data = $this->send_reuqest('PUT', '/dns/records/' . $this->domain, $param);
return !isset($data);
}
//获取解析记录操作日志
//修改解析记录备注
public function updateDomainRecordRemark($RecordId, $Remark)
{
return false;
}
//获取解析线路列表
//删除解析记录
public function deleteDomainRecord($RecordId)
{
$array = explode("|", $RecordId);
@ -271,66 +214,25 @@ class spaceship implements DnsInterface
$name = $array[1];
$address = $array[2];
$mx = $array[3];
if ('MX' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'exchange' => $address,
'preference' => (int)$mx,
]
];
} else if ('TXT' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'value' => $address,
]
];
} else if ('CNAME' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'cname' => $address,
]
];
} else if ('ALIAS' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'aliasName' => $address,
]
];
} else {
$param = [
[
'type' => $type,
'name' => $name,
'address' => $address,
]
];
}
$item = $this->convertRecordItem($name, $type, $address, $mx);
$param = [$item];
$data = $this->send_reuqest('DELETE', '/dns/records/' . $this->domain, $param);
return !isset($data);
}
//获取域名信息
//设置解析记录状态
public function setDomainRecordStatus($RecordId, $Status)
{
return false;
}
//获取域名最低TTL
//获取解析记录操作日志
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
{
return false;
}
//获取解析线路列表
public function getRecordLine()
{
return ['default' => ['name' => '默认', 'parent' => null]];
@ -350,4 +252,43 @@ class spaceship implements DnsInterface
{
return false;
}
}
private function send_reuqest($method, $path, $params = null)
{
$url = $this->baseUrl . $path;
$headers = [
'X-API-Key' => $this->apiKey,
'X-API-Secret' => $this->apiSecret,
];
$body = '';
if ($method == 'GET') {
if ($params) {
$url .= '?' . http_build_query($params);
}
} else {
$body = json_encode($params);
$headers['Content-Type'] = 'application/json';
}
try {
$response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
} catch (\Exception $e) {
$this->setError($e->getMessage());
return false;
}
$arr = json_decode($response['body'], true);
if ($response['code'] == 200 || $response['code'] == 204) {
return $arr;
} elseif (isset($arr['detail'])) {
$this->setError($arr['detail']);
return false;
} else {
$this->setError('http code: ' . $response['code']);
return false;
}
}
private function setError($message)
{
$this->error = $message;
}
}

254
app/lib/dns/tencenteo.php Normal file
View File

@ -0,0 +1,254 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
use app\lib\client\TencentCloud;
use Exception;
class tencenteo implements DnsInterface
{
private $SecretId;
private $SecretKey;
private $endpoint = "teo.tencentcloudapi.com";
private $service = "teo";
private $version = "2022-09-01";
private $error;
private $domain;
private $domainid;
private $domainInfo;
private TencentCloud $client;
public function __construct($config)
{
$this->SecretId = $config['SecretId'];
$this->SecretKey = $config['SecretKey'];
if (isset($config['site_type']) && $config['site_type'] == 'intl') {
$this->endpoint = "teo.intl.tencentcloudapi.com";
}
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new TencentCloud($this->SecretId, $this->SecretKey, $this->endpoint, $this->service, $this->version, null, $proxy);
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
}
public function getError()
{
return $this->error;
}
public function check()
{
if ($this->getDomainList() != false) {
return true;
}
return false;
}
//获取域名列表
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20)
{
$action = 'DescribeZones';
$offset = ($PageNumber - 1) * $PageSize;
$filters = [['Name' => 'zone-type', 'Values' => ['full']]];
if (!isNullOrEmpty($KeyWord)) {
$filters[] = ['Name' => 'zone-name', 'Values' => [$KeyWord]];
}
$param = ['Offset' => $offset, 'Limit' => $PageSize, 'Filters' => $filters];
$data = $this->send_request($action, $param);
if ($data) {
$list = [];
foreach ($data['Zones'] as $row) {
$list[] = [
'DomainId' => $row['ZoneId'],
'Domain' => $row['ZoneName'],
'RecordCount' => 0,
];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取解析记录列表
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$offset = ($PageNumber - 1) * $PageSize;
$action = 'DescribeDnsRecords';
$filters = [];
if (!isNullOrEmpty($SubDomain)) {
$name = $SubDomain == '@' ? $this->domain : $SubDomain . '.' . $this->domain;
$filters[] = ['Name' => 'name', 'Values' => [$name]];
} elseif (!isNullOrEmpty($KeyWord)) {
$name = $KeyWord == '@' ? $this->domain : $KeyWord . '.' . $this->domain;
$filters[] = ['Name' => 'name', 'Values' => [$name]];
}
if (!isNullOrEmpty($Value)) {
$filters[] = ['Name' => 'content', 'Values' => [$Value], 'Fuzzy' => true];
}
if (!isNullOrEmpty($Type)) {
$filters[] = ['Name' => 'type', 'Values' => [$Type]];
}
$param = ['ZoneId' => $this->domainid, 'Offset' => $offset, 'Limit' => $PageSize, 'Filters' => $filters];
$data = $this->send_request($action, $param);
if ($data) {
$list = [];
foreach ($data['DnsRecords'] as $row) {
$name = substr($row['Name'], 0, - (strlen($this->domain) + 1));
if ($name == '') $name = '@';
$list[] = [
'RecordId' => $row['RecordId'],
'Domain' => $this->domain,
'Name' => $name,
'Type' => $row['Type'],
'Value' => $row['Content'],
'Line' => $row['Location'],
'TTL' => $row['TTL'],
'MX' => $row['Priority'],
'Status' => $row['Status'] == 'enable' ? '1' : '0',
'Weight' => $row['Weight'] == -1 ? null : $row['Weight'],
'Remark' => null,
'UpdateTime' => $row['ModifiedOn'],
];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
{
if ($SubDomain == '') $SubDomain = '@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId)
{
$action = 'DescribeDnsRecords';
$param = ['ZoneId' => $this->domainid, 'Filters' => [['Name' => 'id', 'Values' => [$RecordId]]]];
$data = $this->send_request($action, $param);
if ($data) {
$row = $data['DnsRecords'][0];
$name = substr($row['Name'], 0, - (strlen($this->domain) + 1));
if ($name == '') $name = '@';
return [
'RecordId' => $row['RecordId'],
'Domain' => $this->domain,
'Name' => $name,
'Type' => $row['Type'],
'Value' => $row['Content'],
'Line' => $row['Location'],
'TTL' => $row['TTL'],
'MX' => $row['Priority'],
'Status' => $row['Status'] == 'enable' ? '1' : '0',
'Weight' => $row['Weight'] == -1 ? null : $row['Weight'],
'Remark' => null,
'UpdateTime' => $row['ModifiedOn'],
];
}
return false;
}
//添加解析记录
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$action = 'CreateDnsRecord';
if ($Name == '@') {
$Name = $this->domain;
} else {
$Name = $Name . '.' . $this->domain;
}
$param = ['ZoneId' => $this->domainid, 'Name' => $Name, 'Type' => $Type, 'Content' => $Value, 'Location' => $Line, 'TTL' => intval($TTL), 'Weight' => empty($Weight) ? -1 : intval($Weight)];
if ($Type == 'MX') $param['Priority'] = intval($MX);
$data = $this->send_request($action, $param);
return is_array($data) ? $data['RecordId'] : false;
}
//修改解析记录
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$action = 'ModifyDnsRecord';
if ($Name == '@') {
$Name = $this->domain;
} else {
$Name = $Name . '.' . $this->domain;
}
$param = ['ZoneId' => $this->domainid, 'DnsRecordId' => $RecordId, 'Name' => $Name, 'Type' => $Type, 'Content' => $Value, 'Location' => $Line, 'TTL' => intval($TTL), 'Weight' => empty($Weight) ? -1 : intval($Weight)];
if ($Type == 'MX') $param['Priority'] = intval($MX);
$data = $this->send_request($action, $param);
return is_array($data);
}
//修改解析记录备注
public function updateDomainRecordRemark($RecordId, $Remark)
{
return false;
}
//删除解析记录
public function deleteDomainRecord($RecordId)
{
$action = 'DeleteDnsRecords';
$param = ['ZoneId' => $this->domainid, 'RecordIds' => [$RecordId]];
$data = $this->send_request($action, $param);
return is_array($data);
}
//设置解析记录状态
public function setDomainRecordStatus($RecordId, $Status)
{
$action = 'ModifyDnsRecordsStatus';
$param = ['ZoneId' => $this->domainid];
if ($Status == '1') $param['RecordsToEnable'] = [$RecordId];
else $param['RecordsToDisable'] = [$RecordId];
$data = $this->send_request($action, $param);
return is_array($data);
}
//获取解析记录操作日志
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
{
return false;
}
//获取解析线路列表
public function getRecordLine()
{
return ['Default' => ['name' => '默认', 'parent' => null]];
}
//获取域名概览信息
public function getDomainInfo()
{
return false;
}
//获取域名最低TTL
public function getMinTTL()
{
return 60;
}
public function addDomain($Domain)
{
return false;
}
private function send_request($action, $param)
{
try{
return $this->client->request($action, $param);
}catch(Exception $e){
$this->setError($e->getMessage());
return false;
}
}
private function setError($message)
{
$this->error = $message;
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
}
}

View File

@ -19,8 +19,8 @@ class west implements DnsInterface
public function __construct($config)
{
$this->username = $config['ak'];
$this->api_password = $config['sk'];
$this->username = $config['username'];
$this->api_password = $config['api_password'];
$this->domain = $config['domain'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}

View File

@ -13,7 +13,7 @@ class AuthUser
$islogin = false;
$cookie = cookie('user_token');
$user = null;
if ($cookie && config_get('sys_key')) {
if ($cookie && config_get('sys_key') && strpos($request->url(), '/install') === false) {
$token = authcode($cookie, 'DECODE', config_get('sys_key'));
if ($token) {
list($type, $uid, $sid, $expiretime) = explode("\t", $token);

View File

@ -30,6 +30,16 @@ class LoadConfig
return $next($request);
}
}
if (!checkTableExists('config') && !checkTableExists('user')) {
if (strpos($request->url(), '/install') === false) {
return redirect((string)url('/install'))->header([
'Cache-Control' => 'no-store, no-cache, must-revalidate',
'Pragma' => 'no-cache',
]);
} else {
return $next($request);
}
}
try {
$res = Db::name('config')->cache('configs', 0)->column('value', 'key');

View File

@ -70,8 +70,15 @@ class CertDeployService
$this->saveResult(-1, $e->getMessage(), date('Y-m-d H:i:s', time() + (array_key_exists($this->task['retry'], self::$retry_interval) ? self::$retry_interval[$this->task['retry']] : 3600)));
throw $e;
} finally {
if($this->info){
Db::name('cert_deploy')->where('id', $this->task['id'])->update(['info' => json_encode($this->info)]);
if ($this->info && is_array($this->info)) {
if (isset($this->info['config']) && is_array($this->info['config'])) {
$config = array_merge(json_decode($this->task['config'], true), $this->info['config']);
Db::name('cert_deploy')->where('id', $this->task['id'])->update(['config' => json_encode($config)]);
unset($this->info['config']);
}
if (!empty($this->info)) {
Db::name('cert_deploy')->where('id', $this->task['id'])->update(['info' => json_encode($this->info)]);
}
}
}
}

View File

@ -19,7 +19,7 @@ class OptimizeService
public static function get_license($api, $key)
{
if ($api == 2) {
throw new Exception('当前接口暂不支持');
throw new Exception('xingpingcn.top 接口免费使用,无需密钥,无积分限制');
} elseif ($api == 1) {
$url = 'https://api.hostmonit.com/get_license?license='.$key;
} else {
@ -39,7 +39,9 @@ class OptimizeService
public function get_ip_address($cdn_type = 1, $ip_type = 'v4')
{
$api = config_get('optimize_ip_api', 0);
if ($api == 1) {
if ($api == 2) {
return $this->get_ip_address_xingpingcn($ip_type);
} elseif ($api == 1) {
$url = 'https://api.hostmonit.com/get_optimization_ip';
} else {
$url = 'https://www.wetest.vip/api/cf2dns/';
@ -70,6 +72,59 @@ class OptimizeService
}
}
/**
* xingpingcn.top 获取优选IP数据
* @param string $ip_type IP类型 v4/v6
* @return array
* @throws Exception
*/
private function get_ip_address_xingpingcn($ip_type = 'v4')
{
if ($ip_type == 'v6') {
throw new Exception('xingpingcn.top 接口暂不支持IPv6');
}
$proxy = config_get('optimize_ip_proxy', '');
if (!empty($proxy)) {
$proxy = trim($proxy);
if (filter_var($proxy, FILTER_VALIDATE_URL) === false) {
throw new Exception('无效的代理地址配置URL 格式错误');
}
$scheme = parse_url($proxy, PHP_URL_SCHEME);
if (!in_array($scheme, ['http', 'https'], true)) {
throw new Exception('无效的代理地址配置:仅支持 http 和 https 协议');
}
$url = rtrim($proxy, '/') . '/xingpingcn/enhanced-FaaS-in-China/refs/heads/main/Cf.json';
} else {
$url = 'https://raw.githubusercontent.com/xingpingcn/enhanced-FaaS-in-China/refs/heads/main/Cf.json';
}
$response = get_curl($url);
if ($response === '') {
throw new Exception('获取优选IP数据失败网络请求失败请检查网络连接或代理地址');
}
$arr = json_decode($response, true);
if (isset($arr['Cf']['result'])) {
$result = $arr['Cf']['result'];
$info = [];
// 转换格式dianxin->CT, liantong->CU, yidong->CM, default->DEF
if (isset($result['dianxin']) && is_array($result['dianxin'])) {
$info['CT'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['dianxin']);
}
if (isset($result['liantong']) && is_array($result['liantong'])) {
$info['CU'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['liantong']);
}
if (isset($result['yidong']) && is_array($result['yidong'])) {
$info['CM'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['yidong']);
}
// 不使用他的默认线路数据, 因为这真的是默认. 由后续逻辑自己决定是否把CT线路当DEF来用
// if (isset($result['default']) && is_array($result['default'])) {
// $info['DEF'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['default']);
// }
return $info;
} else {
throw new Exception('获取优选IP数据失败接口返回数据格式错误');
}
}
public function get_ip_address2($cdn_type = 1, $ip_type = 'v4')
{
$key = $cdn_type.'_'.$ip_type;
@ -132,7 +187,7 @@ class OptimizeService
continue;
}
$drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.ak,B.sk,B.ext')->find();
$drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type')->find();
if (!$drow) {
throw new Exception('域名不存在ID'.$row['did'].'');
}

View File

@ -33,7 +33,7 @@ class ScheduleService
public function execute_one($row)
{
$drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.ak,B.sk,B.ext')->find();
$drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.config')->find();
if (!$drow) throw new Exception('域名不存在');
Db::name('sctask')->where('id', $row['id'])->update(['updatetime' => time()]);

View File

@ -71,7 +71,7 @@ class TaskRunner
}
if ($action > 0) {
$drow = $this->db()->name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.ak,B.sk,B.ext')->find();
$drow = $this->db()->name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.config')->find();
if (!$drow) {
echo '域名不存在ID'.$row['did'].''."\n";
$this->closeDb();

View File

@ -5,7 +5,7 @@ CREATE TABLE `dnsmgr_config` (
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `dnsmgr_config` VALUES ('version', '1040');
INSERT INTO `dnsmgr_config` VALUES ('version', '1045');
INSERT INTO `dnsmgr_config` VALUES ('notice_mail', '0');
INSERT INTO `dnsmgr_config` VALUES ('notice_wxtpl', '0');
INSERT INTO `dnsmgr_config` VALUES ('mail_smtp', 'smtp.qq.com');
@ -15,10 +15,8 @@ DROP TABLE IF EXISTS `dnsmgr_account`;
CREATE TABLE `dnsmgr_account` (
`id` int(11) unsigned NOT NULL auto_increment,
`type` varchar(20) NOT NULL,
`ak` varchar(256) DEFAULT NULL,
`sk` varchar(256) DEFAULT NULL,
`ext` varchar(256) DEFAULT NULL,
`proxy` tinyint(1) NOT NULL DEFAULT '0',
`name` varchar(255) NOT NULL,
`config` text DEFAULT NULL,
`remark` varchar(100) DEFAULT NULL,
`addtime` datetime DEFAULT NULL,
PRIMARY KEY (`id`)

View File

@ -185,4 +185,8 @@ CREATE TABLE IF NOT EXISTS `dnsmgr_sctask` (
`remark` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `did` (`did`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `dnsmgr_account`
ADD COLUMN `config` text DEFAULT NULL,
CHANGE COLUMN `ak` `name` varchar(255) NOT NULL;

View File

@ -257,6 +257,7 @@ class MsgNotice
public static function send_webhook($title, $content)
{
$url = config_get('webhook_url');
$atuser = config_get('webhook_user');
if (!$url || !parse_url($url)) return false;
if (strpos($url, 'oapi.dingtalk.com')) {
$content = '### '.$title." \n ".str_replace("\n", " \n ", $content);
@ -267,6 +268,14 @@ class MsgNotice
'text' => $content,
],
];
if (!empty($atuser)) {
if ($atuser == 'all') {
$post['at'] = ['isAtAll' => true];
} else {
$atusers = explode(',', $atuser);
$post['at'] = ['atMobiles' => $atusers, 'isAtAll' => false];
}
}
} elseif (strpos($url, 'qyapi.weixin.qq.com')) {
$content = '## '.$title."\n".$content;
$post = [
@ -276,11 +285,63 @@ class MsgNotice
],
];
} elseif (strpos($url, 'open.feishu.cn') || strpos($url, 'open.larksuite.com')) {
$content = str_replace(['\*', '**'], ['*', ''], strip_tags($content));
$content = str_replace('<font color="warning">', '<font color="red">', $content);
if (!empty($atuser)) {
if ($atuser == 'all') {
$content .= "\n".'<at id=all></at> ';
} else {
$atusers = explode(',', $atuser);
$content .= "\n";
foreach ($atusers as $u) {
$content .= '<at user_id="'.$u.'"></at> ';
}
}
}
$template = 'blue';
if(strpos($title, '发生告警') !== false || strpos($title, '失败') !== false) $template = 'red';
else if(strpos($title, '恢复正常') !== false) $template = 'green';
else if(strpos($title, '到期提醒') !== false) $template = 'yellow';
$post = [
'msg_type' => 'text',
'content' => [
'text' => $content,
'msg_type' => 'interactive',
'card' => [
'schema' => '2.0',
'config' => [
'update_multi' => true,
'style' => [
'text_size' => [
'normal_v2' => [
'default' => 'normal',
'pc' => 'normal',
'mobile' => 'heading',
],
],
],
],
'header' => [
'title' => [
'tag' => 'plain_text',
'content' => $title,
],
'subtitle' => [
'tag' => 'plain_text',
'content' => '',
],
'template' => $template,
'padding' => '12px 12px 12px 12px',
],
'body' => [
'direction' => 'vertical',
'padding' => '12px 12px 12px 12px',
'elements' => [
[
'tag' => 'markdown',
'content' => $content,
'text_align' => 'left',
'text_size' => 'normal_v2',
'margin' => '0px 0px 0px 0px',
]
],
],
],
];
} else {

View File

@ -124,7 +124,7 @@ $(document).ready(function(){
field: 'end_day',
title: '到期时间',
formatter: function(value, row, index) {
if(value){
if(value != null){
if(value > 7){
return '<span title="'+row.expiretime+'" data-toggle="tooltip" data-placement="right" style="color:green">剩余' + value + '天<span>';
}else if(value > 0){

View File

@ -110,7 +110,7 @@
<a href="/domain"><i class="fa fa-list-ul fa-fw"></i> <span>域名管理</span></a>
</li>
{if request()->user['level'] eq 2}
<li class="{:checkIfActive('account')}">
<li class="{:checkIfActive('account,account_add')}">
<a href="/account"><i class="fa fa-lock fa-fw"></i> <span>域名账户</span></a>
</li>
<li class="treeview {:checkIfActive('overview,task,taskinfo,taskform')}">

View File

@ -1,70 +1,6 @@
{extend name="common/layout" /}
{block name="title"}域名账户{/block}
{block name="main"}
<div class="modal" id="modal-store" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content animated flipInX">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span
aria-hidden="true">&times;</span><span
class="sr-only">Close</span></button>
<h4 class="modal-title" id="modal-title">添加/修改域名账户</h4>
</div>
<div class="modal-body">
<form class="form-horizontal" id="form-store">
<input type="hidden" name="action"/>
<input type="hidden" name="id"/>
<div class="form-group">
<label class="col-sm-3 control-label">所属平台</label>
<div class="col-sm-9">
<select name="type" class="form-control">
{foreach $dnsconfig as $k=>$v}
<option value="{$k}">{$v['name']}</option>
{/foreach}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" id="ak_name">AccessKey</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="ak" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" id="sk_name">SecretKey</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="sk" required>
</div>
</div>
<div class="form-group" id="ext_name_div" style="display:none;">
<label class="col-sm-3 control-label no-padding-right" id="ext_name">扩展字段</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="ext" placeholder="">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" id="ext_name">使用代理</label>
<div class="col-sm-9">
<label class="radio-inline"><input type="radio" name="proxy" value="0">
</label><label class="radio-inline"><input type="radio" name="proxy" value="1">
</label>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">备注</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="remark" placeholder="备注选填">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="store" onclick="save()">保存</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default panel-intro">
@ -73,11 +9,11 @@
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="kw" placeholder="AccessKey或备注">
<input type="text" class="form-control" name="kw" placeholder="账户名称或备注">
</div>
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
<a href="javascript:searchClear()" class="btn btn-default" title="刷新域名账户列表"><i class="fa fa-refresh"></i> 刷新</a>
<a href="javascript:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
<a href="/account/add" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
</form>
<table id="listTable">
@ -93,7 +29,6 @@
<script src="/static/js/bootstrap-table-page-jump-to-1.21.4.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
var dnsconfig = {$dnsconfig|json_encode|raw};
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 15;
@ -114,12 +49,12 @@ $(document).ready(function(){
field: 'typename',
title: '所属平台',
formatter: function(value, row, index) {
return '<img src="/static/images/'+row.type+'.ico" class="type-logo"></img>'+value;
return '<img src="/static/images/'+row.icon+'" class="type-logo"></img>'+value;
}
},
{
field: 'ak',
title: 'AccessKey'
field: 'name',
title: '账户名称'
},
{
field: 'remark',
@ -133,101 +68,19 @@ $(document).ready(function(){
field: 'action',
title: '操作',
formatter: function(value, row, index) {
var html = '<a href="javascript:editframe('+row.id+')" class="btn btn-info btn-xs">编辑</a> <a href="javascript:delItem('+row.id+')" class="btn btn-danger btn-xs">删除</a>';
var html = '<a href="/account/edit?id='+row.id+'" class="btn btn-info btn-xs">编辑</a> <a href="javascript:delItem('+row.id+')" class="btn btn-danger btn-xs">删除</a> <a href="/domain?aid='+row.id+'" class="btn btn-default btn-xs">域名</a>';
return html;
}
},
],
})
$("select[name=type]").change(function(){
var type = $(this).val();
if(dnsconfig[type] == undefined) return;
$("#ak_name").html(dnsconfig[type].config.ak);
$("#sk_name").html(dnsconfig[type].config.sk);
if(dnsconfig[type].config.ext == undefined){
$("#ext_name_div").hide();
}else{
$("#ext_name_div").show();
$("#ext_name").html(dnsconfig[type].config.ext);
}
});
})
function addframe(){
$("#modal-store").modal('show');
$("#modal-title").html("添加域名账户");
$("#form-store input[name=action]").val("add");
$("#form-store input[name=id]").val('');
$("#form-store input[name=ak]").val('');
$("#form-store input[name=sk]").val('');
$("#form-store input[name=ext]").val('');
$("#form-store input[name=proxy]").eq(0).prop('checked',true);
$("#form-store input[name=remark]").val('');
$("select[name=type]").change();
}
function editframe(id){
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/account/op/act/get',
data : {id: id},
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
$("#modal-store").modal('show');
$("#modal-title").html("修改域名账户");
$("#form-store input[name=action]").val("edit");
$("#form-store input[name=id]").val(data.data.id);
$("#form-store select[name=type]").val(data.data.type);
$("#form-store input[name=ak]").val(data.data.ak);
$("#form-store input[name=sk]").val(data.data.sk);
$("#form-store input[name=ext]").val(data.data.ext);
$("#form-store input[name=proxy]").eq(data.data.proxy).prop('checked',true);
$("#form-store input[name=remark]").val(data.data.remark);
$("select[name=type]").change();
}else{
layer.alert(data.msg, {icon: 2})
}
}
});
}
function save(){
if($("#form-store input[name=username]").val()==''){
layer.alert('请确保各项不能为空!');return false;
}
var act = $("#form-store input[name=action]").val();
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/account/op/act/'+act,
data : $("#form-store").serialize(),
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg,{
icon: 1,
closeBtn: false
}, function(){
layer.closeAll();
$("#modal-store").modal('hide');
searchRefresh();
});
}else{
layer.alert(data.msg, {icon: 2})
}
}
});
}
function delItem(id) {
var confirmobj = layer.confirm('确定要删除此域名账户吗?', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要删除此域名账户吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/account/op/act/del',
url : '/account/del',
data : {id: id},
dataType : 'json',
success : function(data) {
@ -240,8 +93,6 @@ function delItem(id) {
}
}
});
}, function(){
layer.close(confirmobj);
});
}
</script>

View File

@ -0,0 +1,243 @@
{extend name="common/layout" /}
{block name="title"}域名账户{/block}
{block name="main"}
<style>
.tips{color: #f6a838; padding-left: 5px;}
.input-note{color: green;}
.control-label[is-required]:before {
content: "*";
color: #f56c6c;
margin-right: 4px;
}
</style>
<div class="row" id="app">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title"><a href="javascript:window.history.back()" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>{if $action=='edit'}编辑{else}添加{/if}域名账户</h3></div>
<div class="panel-body">
<form onsubmit="return false" method="post" class="form-horizontal" role="form" id="accountform">
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" is-required>账户类型</label>
<div class="col-sm-6">
<select name="type" class="form-control" v-model="set.type">
<option v-for="(item, key) in typeList" :value="key" :data-icon="item.icon">{{item.name}}</option>
</select>
</div>
</div>
<div v-for="(item,name) in inputs" v-show="isShow(item.show)">
<div class="form-group" v-if="item.type=='input'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<input type="text" class="form-control" :name="name" v-model="config[name]" :placeholder="item.placeholder" :required="item.required" :disabled="item.disabled" :data-bv-id="item.validator=='id'" :data-bv-phone="item.validator=='phone'" :data-bv-numeric="item.validator=='numeric'" :data-bv-digits="item.validator=='digits'" :data-bv-integer="item.validator=='integer'" :data-bv-email="item.validator=='email'" :data-bv-uri="item.validator=='uri'" :min="item.min" :max="item.max"><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='textarea'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<textarea class="form-control" :name="name" v-model="config[name]" :placeholder="item.placeholder" :required="item.required" :disabled="item.disabled"></textarea><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='select'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<select class="form-control" :name="name" v-model="config[name]" :required="item.required" :disabled="item.disabled" :placeholder="item.placeholder">
<option v-for="option in item.options" :value="option.value">{{option.label}}</option>
</select><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='radio'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<label class="radio-inline" v-for="(optionname, optionvalue) in item.options">
<input type="radio" :name="name" :value="optionvalue" v-model="config[name]" :disabled="item.disabled"> {{optionname}}
</label><br/><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='checkbox'">
<div class="col-sm-offset-3 col-sm-7">
<div class="checkbox">
<label>
<input type="checkbox" :name="name" v-model="config[name]" :disabled="item.disabled"> {{item.name}}
</label>
</div>
</div>
</div>
<div class="form-group" v-if="item.type=='checkboxes'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<label class="checkbox-inline" v-for="(optionname, optionvalue) in item.options">
<input type="checkbox" :name="name" :value="optionvalue" v-model="config[name]" :disabled="item.disabled"> {{optionname}}
</label><br/><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">备注</label>
<div class="col-sm-6">
<input type="text" name="remark" v-model="set.remark" placeholder="可留空" class="form-control">
</div>
</div>
<div class="form-group" v-show="note">
<div class="col-sm-offset-3 col-sm-6">
<div class="alert alert-dismissible alert-info">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<strong>提示:</strong><span v-html="note"></span>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-6"><button type="button" class="btn btn-primary" @click="submit">提交</button></div>
</div>
</form>
</div>
</div>
{/block}
{block name="script"}
<script src="/static/js/vue-2.7.16.min.js"></script>
<script src="/static/js/layer/layer.js"></script>
<script src="/static/js/select2-4.0.13.min.js"></script>
<script src="/static/js/select2-i18n-zh-CN-4.0.13.min.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>
var info = {$info|json_encode|raw};
var typeList = {$typeList|json_encode|raw};
new Vue({
el: '#app',
data: {
action: '{$action}',
set: {
id: '',
type: '',
name: '',
config : '',
remark: '',
},
inputs: {},
config: {},
typeList: typeList,
note: '',
},
watch: {
'set.type': function(val){
if(this.action == 'add' && val && typeList[val]){
this.inputs = typeList[val].config;
this.note = typeList[val].note;
this.config = {};
$.each(this.inputs, (name, item) => {
if(typeof item.value == 'undefined'){
if(item.type == 'checkbox'){
item.value = false;
}else if(item.type == 'checkboxes'){
item.value = [];
}else{
item.value = null;
}
}
this.$set(this.config, name, item.value)
})
}
}
},
mounted() {
if(this.action == 'edit'){
Object.keys(info).forEach((key) => {
this.set[key] = info[key]
})
var config = JSON.parse(info.config);
this.inputs = typeList[this.set.type].config;
this.note = typeList[this.set.type].note;
$.each(this.inputs, (name, item) => {
if(typeof config[name] != 'undefined'){
item.value = config[name];
}
if(typeof item.value == 'undefined'){
if(item.type == 'checkbox'){
item.value = false;
}else if(item.type == 'checkboxes'){
item.value = [];
}else{
item.value = null;
}
}
this.$set(this.config, name, item.value)
})
}else{
this.set.type = Object.keys(typeList)[0]
}
var that = this;
this.$nextTick(function () {
$('[data-toggle="tooltip"]').tooltip();
function formatType(option) {
if (!option.id) return option.text;
var icon = $(option.element).data('icon');
if (icon) {
return $('<span><img src="/static/images/' + icon + '" class="type-logo" />' + option.text + '</span>');
}
return option.text;
}
$('select[name=type]').select2({
templateResult: formatType,
templateSelection: formatType,
minimumResultsForSearch: Infinity,
width: '100%'
}).on('select2:select', function(e){
that.set.type = e.params.data.id;
});
if(that.action == 'edit'){
$('select[name=type]').val(that.set.type).trigger('change.select2');
}
})
},
methods: {
submit(){
var that=this;
Object.keys(this.config).forEach((key) => {
if(this.config[key] && typeof this.config[key] == 'string'){
this.config[key] = this.trim(this.config[key]);
}
})
this.set.config = JSON.stringify(this.config);
this.set.name = this.config[Object.keys(this.config)[0]];
let loading = layer.msg('正在进行账户有效性检查', {icon: 16,shade: 0.1,time: 0});
$.ajax({
type: "POST",
url: "",
data: this.set,
dataType: 'json',
success: function(data) {
layer.close(loading);
if(data.code == 0){
layer.alert(data.msg, {icon: 1}, function(){
window.location.href = '/account';
});
}else{
layer.alert(data.msg, {icon: 2});
}
},
error: function(data){
layer.close(loading);
layer.msg('服务器错误');
}
});
},
isShow(show){
if(typeof show == 'boolean' && show){
return show;
}else if(typeof show == 'string' && show){
var that=this;
Object.keys(this.config).forEach((key) => {
show = show.replace(new RegExp(key, 'g'), 'that.config["'+key+'"]')
})
return eval(show);
}else{
return true;
}
},
trim(str){
return str.replace(/(^\s*)|(\s*$)/g, "");
}
},
});
</script>
{/block}

View File

@ -48,6 +48,12 @@
<input type="text" class="form-control" name="ttl" value="600" placeholder="指解析结果在DNS服务器中的缓存时间" required min="{$minTTL}">
</div>
</div>
{if $dnsconfig.remark == 2}<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">备注</label>
<div class="col-sm-6">
<input type="text" class="form-control" name="remark" placeholder="">
</div>
</div>{/if}
<div class="form-group">
<div class="col-sm-offset-3 col-sm-6"><button type="button" class="btn btn-primary" onclick="save()">添加</button></div>
</div>

View File

@ -81,7 +81,7 @@ tbody tr>td:nth-child(3){min-width:300px;word-break:break-all;}
<tbody>
<tr v-for="item in domainList">
<td>{{item.id}}</td>
<td><img :src="'/static/images/'+item.type+'.ico'" class="type-logo"></img><a :href="'/record/'+item.id" target="_blank">{{item.name}}</a></td>
<td><img :src="'/static/images/'+item.icon+''" class="type-logo"></img><a :href="'/record/'+item.id" target="_blank">{{item.name}}</a></td>
<td v-html="item.result"></td>
</tr>
</tbody>

View File

@ -127,6 +127,8 @@
<div class="panel-body">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<input type="hidden" name="id" value="">
<input type="hidden" name="aid" value="">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="kw" placeholder="域名或备注">
@ -196,7 +198,7 @@ $(document).ready(function(){
field: 'typename',
title: '平台账户',
formatter: function(value, row, index) {
return '<img src="/static/images/'+row.type+'.ico" class="type-logo"></img>'+(row.aremark?row.aremark:value+'('+row.aid+')');
return '<img src="/static/images/'+row.icon+'" class="type-logo"></img>'+(row.aremark?row.aremark:value+'('+row.aid+')');
}
},
{
@ -426,9 +428,7 @@ function saveEdit(){
});
}
function delItem(id) {
var confirmobj = layer.confirm('确定要删除此域名吗?删除域名不会影响已添加的解析', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要删除此域名吗?删除域名不会影响已添加的解析', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -445,8 +445,6 @@ function delItem(id) {
}
}
});
}, function(){
layer.close(confirmobj);
});
}
function getDomainList(){
@ -515,9 +513,7 @@ function operation(action){
window.location.href = '/record/batchedit';
return;
}else if(action == 'delete'){
var confirmobj = layer.confirm('确定要删除所选域名吗?', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要删除所选域名吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -535,13 +531,9 @@ function operation(action){
}
}
});
}, function(){
layer.close(confirmobj);
});
}else if(action == 'updateexpire'){
var confirmobj = layer.confirm('提交后将异步刷新所选域名的到期时间', {
btn: ['确定','取消']
}, function(){
layer.confirm('提交后将异步刷新所选域名的到期时间', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -559,8 +551,6 @@ function operation(action){
}
}
});
}, function(){
layer.close(confirmobj);
});
}else{
var is_notice = action == 'opennotice' ? 1 : 0;

View File

@ -0,0 +1,556 @@
{extend name="common/layout" /}
{block name="title"}解析管理 - {$domainName}{/block}
{block name="main"}
<style>
td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;}
.dns-parent-row { cursor: pointer; }
.dns-child-row td { background: #fafafa; }
.dns-child-empty td { background:#fafafa; }
.glyphicon-spin { animation: spin 1s infinite linear; }
@keyframes spin { from {transform:rotate(0deg);} to {transform:rotate(360deg);} }
.form-group .radio-inline {position: unset;}
.tips {color: #f6a838;padding-left: 5px;}
.text-remark {margin-left: 10px;color: #329a29;font-size: 12px;}
</style>
<div class="row" id="app">
<div class="modal" id="modal-store" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content animated flipInX">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span
aria-hidden="true">&times;</span><span
class="sr-only">Close</span></button>
<h4 class="modal-title" id="modal-title">{{form.action=='add'?'添加解析':'修改解析'}}</h4>
</div>
<div class="modal-body">
<form class="form-horizontal" id="form-store">
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">主机记录</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="name" placeholder="填写域名前缀,支持多级" v-model="form.name" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">记录类型</label>
<div class="col-sm-9">
<select name="type" class="form-control" v-model="form.type">
<option value="A">A</option>
<option value="CNAME">CNAME</option>
<option value="AAAA">AAAA</option>
<option value="NS">NS</option>
<option value="MX">MX</option>
<option value="TXT">TXT</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">线路类型</label>
<div class="col-sm-9" id="line_list">
<select name="line" class="form-control" v-model="form.line">
<option v-for="line in recordLine" :value="line.id">{{line.name}}</option>
</select>
</div>
</div>
<div class="form-group" v-show="form.type=='A' || form.type=='CNAME'">
<label class="col-sm-3 control-label">模式</label>
<div class="col-sm-9" id="line_list">
<label class="radio-inline"><input type="radio" name="mode" value="1" v-model="form.mode"> 普通<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="每次权威 DNS 查询都将按您填写的顺序返回解析结果,查询性能更好。" class="tips"><i class="fa fa-question-circle"></i></span></label>
<label class="radio-inline" v-show="form.type=='A'"><input type="radio" name="mode" value="2" v-model="form.mode"> 轮询<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="每次权威 DNS 查询的解析结果排序都将会较上一次发生变化,业务负载更均衡。" class="tips"><i class="fa fa-question-circle"></i></span></label>
<label class="radio-inline" v-show="form.type=='A'"><input type="radio" name="mode" value="4" v-model="form.mode"> 智能<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="根据访问来源的运营商及地理位置将解析结果按匹配度排序并最多返回前 5 个,可减少您对精细化线路配置的烦恼。" class="tips"><i class="fa fa-question-circle"></i></span></label>
<label class="radio-inline"><input type="radio" name="mode" value="3" v-model="form.mode"> 权重<span title="" data-toggle="tooltip" data-placement="bottom" data-original-title="每次权威 DNS 查询都将根据每组解析结果的权值按比例返回,使得业务负载可以随心所欲。" class="tips"><i class="fa fa-question-circle"></i></span></label>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">记录值</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="value" :placeholder="'输入记录值' + (form.type=='A'&&form.action=='add'?'多个IP用,间隔)':'')" v-model="form.value" required>
</div>
</div>
<div class="form-group" v-show="form.type=='MX'">
<label class="col-sm-3 control-label no-padding-right">MX优先级</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="mx" v-model="form.mx">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">TTL</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="ttl" v-model="form.ttl" placeholder="指解析结果在DNS服务器中的缓存时间" required min="{$minTTL}">
</div>
</div>
<div class="form-group" v-show="(form.type=='A' || form.type=='CNAME') && form.mode=='3'">
<label class="col-sm-3 control-label no-padding-right">权重</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="weight" v-model="form.weight" placeholder="权重值(1-99)" min="1" max="99">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="store" @click="save">保存</button>
</div>
</div>
</div>
</div>
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{if request()->user['type'] eq 'user'}<a href="/domain" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>{/if}{$domainName}</h3>
</div>
<div class="panel-body">
<form class="form-inline" id="searchToolbar" @submit.prevent>
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="keyword" placeholder="输入主机记录" v-model="keyword" @keyup.enter="loadParents">
</div>
<button type="button" class="btn btn-primary" @click="loadParents"><i class="fa fa-search"></i> 搜索</button>
<button type="button" class="btn btn-default" title="刷新解析记录列表" @click="keyword=null;loadParents()"><i class="fa fa-refresh"></i> 刷新</button>
<button type="button" class="btn btn-success" @click="addRecord"><i class="fa fa-plus"></i> 添加记录</button>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">批量操作 <span class="caret"></span></button>
<ul class="dropdown-menu"><li><a href="/record/batchadd/{$domainId}">添加</a></li></ul>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">日志 <span class="caret"></span></button>
<ul class="dropdown-menu"><li><a href="/log?domain={$domainName}">本站日志</a></li></ul>
</div>
</form>
<div class="table-responsive" style="margin-top:15px;">
<table class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>主机记录</th>
<th>线路类型</th>
<th>记录类型</th>
<th>模式</th>
<th style="min-width:150px">记录值</th>
<th>TTL</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<template v-if="loading">
<tr>
<td colspan="8" class="text-muted text-center"><i class="glyphicon glyphicon-refresh glyphicon-spin"></i> 正在加载...</td>
</tr>
</template>
<template v-if="!loading && parents.length === 0">
<tr>
<td colspan="8" class="text-muted text-center">暂无记录</td>
</tr>
</template>
<template v-if="!loading" v-for="p in parents">
<tr :key="'p-' + p.RecordId" class="dns-parent-row" @click="toggleParent(p)">
<td colspan="7">
<span class="text-muted" style="display:inline-block;width:18px;">
<i class="glyphicon"
:class="expandedMap[p.RecordId] ? 'glyphicon-chevron-down' : 'glyphicon-chevron-right'"></i>
</span>
<strong>{{ p.Name }}</strong><span class="text-muted">(共 {{ p.Count }} 条记录)</span><span class="text-remark" v-if="p.Remark"><i class="glyphicon glyphicon-list-alt"></i> {{ p.Remark }}</span>
</td>
<td @click.stop>
<button class="btn btn-xs btn-success" @click="addRecord(p)">添加</button>
<button class="btn btn-xs btn-info" @click="editHostRemark(p)">备注</button>
<button class="btn btn-xs btn-danger" @click="deleteHost(p)">删除</button>
</td>
</tr>
<tr v-if="expandedMap[p.RecordId] && loadingMap[p.RecordId]" class="dns-child-empty">
<td colspan="8" class="text-muted text-center"><i class="glyphicon glyphicon-refresh glyphicon-spin"></i> 正在加载...</td>
</tr>
<tr v-for="c in (expandedMap[p.RecordId] ? (childrenMap[p.RecordId] || []) : [])"
:key="'c-' + c.RecordId"
v-if="expandedMap[p.RecordId] && !loadingMap[p.RecordId]"
class="dns-child-row">
<td></td>
<td>{{ c.LineName }}</td>
<td>{{ c.Type }}</td>
<td>{{ c.Type == 'A' || c.Type == 'CNAME' ? modeList[c.Mode] : '-' }} <span class="label label-info" v-if="(c.Type == 'A' || c.Type == 'CNAME') && c.Mode == 3">{{c.Weight}}</span></td>
<td>{{ c.Value + (c.Type == 'MX' ? ' | ' + c.MX : '') }}<a href="javascript:void(0);" title="复制记录值" @click="copyToClipboard(c, $event)" style="padding-left:6px;"><i class="fa fa-copy"></i></a></td>
<td>{{ c.TTL }}</td>
<td><font color="green" v-if="c.Status=='1'"><i class="fa fa-check-circle"></i>启用</font><font color="orange" v-if="c.Status!='1'"><i class="fa fa-pause-circle"></i>暂停</font></td>
<td>
<button class="btn btn-xs btn-primary" @click="editRecord(c, p.RecordId)">修改</button>
<button class="btn btn-xs btn-warning" @click="setRecordStatus(c, p.RecordId, '0')" v-if="c.Status=='1'">暂停</button>
<button class="btn btn-xs btn-success" @click="setRecordStatus(c, p.RecordId, '1')" v-if="c.Status!='1'">启用</button>
<button class="btn btn-xs btn-danger" @click="deleteRecord(c, p.RecordId)">删除</button>
</td>
</tr>
<tr v-if="expandedMap[p.RecordId] && !loadingMap[p.RecordId] && (childrenMap[p.RecordId] || []).length === 0"
:key="'empty-' + p.RecordId"
class="dns-child-empty">
<td colspan="8" class="text-muted text-center">暂无记录</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="row" style="margin-top:10px;">
<div class="col-sm-6 text-muted" style="padding-top:6px;">
共 {{ total }} 条,当前第 {{ currentPage }} / {{ totalPages }} 页
</div>
<div class="col-sm-6 text-right">
<ul class="pagination pagination-sm" style="margin:0;">
<li :class="{disabled: currentPage === 1}">
<a href="javascript:;" @click="goPage(1)">&laquo;</a>
</li>
<li :class="{disabled: currentPage === 1}">
<a href="javascript:;" @click="goPage(currentPage - 1)">上一页</a>
</li>
<li v-for="n in pageList" :key="'pg-' + n" :class="{active: n === currentPage}">
<a href="javascript:;" @click="goPage(n)">{{ n }}</a>
</li>
<li :class="{disabled: currentPage === totalPages}">
<a href="javascript:;" @click="goPage(currentPage + 1)">下一页</a>
</li>
<li :class="{disabled: currentPage === totalPages}">
<a href="javascript:;" @click="goPage(totalPages)">&raquo;</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="/static/js/vue-2.7.16.min.js"></script>
<script src="/static/js/layer/layer.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>
var recordLine = {$recordLine|json_encode|raw};
var dnsconfig = {$dnsconfig|json_encode|raw};
var defaultLine = recordLine[0].id;
new Vue({
el: '#app',
data: function () {
return {
loading: false,
recordLine: recordLine,
form: {
action: '',
recordid: '',
recordinfo: '',
parentid: '',
name: '',
type: '',
line: '',
mode: '',
value: '',
ttl: 600,
weight: '',
mx: 10,
},
keyword: '',
total: 0,
offset: 0,
limit: 10,
parents: [],
expandedMap: {}, // {pid: true/false}
loadingMap: {}, // {pid: true/false}
childrenMap: {}, // {pid: []} 缓存子列表
modeList: [
'默认',
'普通',
'轮询',
'权重',
'智能',
]
};
},
computed: {
currentPage: function () {
return Math.floor(this.offset / this.limit) + 1;
},
totalPages: function () {
return Math.max(1, Math.ceil(this.total / this.limit));
},
pageList: function () {
// 显示最多 5 个页码,居中
var totalPages = this.totalPages;
var cur = this.currentPage;
var windowSize = 5;
var half = Math.floor(windowSize / 2);
var start = Math.max(1, cur - half);
var end = Math.min(totalPages, start + windowSize - 1);
start = Math.max(1, end - windowSize + 1);
var arr = [];
for (var i = start; i <= end; i++) arr.push(i);
return arr;
}
},
mounted: function () {
this.loadParents();
$('[data-toggle="tooltip"]').tooltip();
$("#form-store").bootstrapValidator();
},
methods: {
loadParents: function () {
var vm = this;
vm.loading = true;
vm.expandedMap = {};
vm.loadingMap = {};
vm.childrenMap = {};
$.ajax({
url: '/record/data/{$domainId}',
method: 'POST',
data: { keyword: vm.keyword, offset: vm.offset, limit: vm.limit },
dataType: 'json'
}).done(function (res) {
vm.loading = false;
vm.total = res.total || 0;
vm.parents = res.rows || [];
}).fail(function () {
layer.msg('加载父级列表失败');
});
},
goPage: function (page) {
if (!page) return;
page = Math.max(1, Math.min(this.totalPages, page));
if (page === this.currentPage) return;
this.offset = (page - 1) * this.limit;
this.loadParents();
},
toggleParent: function (p) {
var pid = p.RecordId;
// 收起
if (this.expandedMap[pid]) {
this.$set(this.expandedMap, pid, false);
return;
}
// 展开
this.$set(this.expandedMap, pid, true);
// 已有缓存就不再请求
if (this.childrenMap[pid]) return;
this.loadChildren(pid);
},
loadChildren: function (pid) {
var vm = this;
vm.$set(vm.loadingMap, pid, true);
$.ajax({
url: '/record/data/{$domainId}',
method: 'POST',
data: { subdomain: pid },
dataType: 'json'
}).done(function (res) {
vm.$set(vm.childrenMap, pid, (res && res.rows) ? res.rows : []);
}).fail(function () {
layer.msg('加载子级列表失败');
vm.$set(vm.childrenMap, pid, []);
}).always(function () {
vm.$set(vm.loadingMap, pid, false);
});
},
addRecord: function (p) {
this.form.action = 'add';
this.form.recordid = '';
this.form.recordinfo = '';
this.form.parentid = p.RecordId || '';
this.form.name = p.Name || '';
this.form.type = 'A';
this.form.line = defaultLine;
this.form.mode = '1';
this.form.value = '';
this.form.ttl = 600;
this.form.weight = '';
this.form.mx = 10;
$("#modal-store").modal('show');
$("#form-store").data("bootstrapValidator").resetForm();
},
editRecord: function (c, parentid) {
this.form.action = 'update';
this.form.recordid = c.RecordId;
this.form.recordinfo = JSON.stringify(c);
this.form.parentid = parentid || '';
this.form.name = c.Name;
this.form.type = c.Type;
this.form.line = c.Line;
this.form.mode = c.Mode;
this.form.value = c.Value;
this.form.ttl = c.TTL;
this.form.weight = c.Weight;
this.form.mx = c.MX || 10;
$("#modal-store").modal('show');
$("#form-store").data("bootstrapValidator").resetForm();
},
save: function () {
$("#form-store").data("bootstrapValidator").validate();
if(!$("#form-store").data("bootstrapValidator").isValid()){
return;
}
var vm = this;
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/record/'+vm.form.action+'/{$domainId}',
data : vm.form,
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg,{
icon: 1,
closeBtn: false
}, function(){
layer.closeAll();
$("#modal-store").modal('hide');
if(vm.form.parentid){
vm.loadChildren(vm.form.parentid);
}else{
vm.loadParents();
}
});
}else{
layer.alert(data.msg, {icon: 2})
}
}
});
},
setRecordStatus: function (c, parentid, status) {
var vm = this;
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/record/status/{$domainId}',
data : { recordid: c.RecordId, status: status, recordinfo: JSON.stringify(c) },
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.closeAll();
layer.msg(status=='1'?'开启成功':'暂停成功', {icon: 1, time:500});
vm.loadChildren(parentid);
}else{
layer.alert(data.msg, {icon: 2})
}
}
});
},
deleteRecord: function (c, parentid) {
var vm = this;
layer.confirm('确定要删除此解析记录吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/record/delete/{$domainId}',
data : { recordid: c.RecordId, recordinfo: JSON.stringify(c) },
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.closeAll();
layer.msg('删除成功', {icon: 1, time:800});
vm.loadChildren(parentid);
}else{
layer.alert(data.msg, {icon: 2})
}
}
});
});
},
deleteHost: function (p) {
var vm = this;
layer.confirm('确定要删除此主机名下所有解析记录吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/record/delete/{$domainId}',
data : { recordid: p.RecordId },
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.closeAll();
layer.msg('删除成功', {icon: 1, time:800});
vm.loadParents();
}else{
layer.alert(data.msg, {icon: 2})
}
}
});
});
},
editHostRemark: function (p) {
var vm = this;
layer.open({
type: 1,
area: ['350px'],
closeBtn: 2,
title: '编辑备注',
content: '<div style="padding:15px"><div class="form-group"><input class="form-control" type="text" name="remark" value="'+(p.Remark==null?'':p.Remark)+'" autocomplete="off" placeholder="备注信息"></div></div>',
btn: ['确认', '取消'],
yes: function(){
var remark = $("input[name='remark']").val();
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/record/remark/{$domainId}',
data : {recordid:p.RecordId, remark:remark},
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.closeAll();
layer.msg('保存成功', {icon: 1, time:800});
vm.loadParents();
}else{
layer.alert(data.msg, {icon:2});
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
}
});
},
copyToClipboard: function (c, event) {
var text = c.Value;
var tempInput = document.createElement('input');
tempInput.style.position = 'absolute';
tempInput.style.left = '-9999px';
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
$(event.target).toggleClass('fa-copy fa-check');
setTimeout(function(){
$(event.target).toggleClass('fa-check fa-copy');
}, 1000);
layer.msg('已复制到剪贴板', {icon: 1, time: 600});
},
}
});
</script>
{/block}

View File

@ -292,7 +292,6 @@ $(document).ready(function(){
formatter: function(value, row, index) {
var copyId = 'copy-value-' + row.RecordId;
if(row.Type == 'MX') {
// 只复制 mx.yandex.net按钮在其右侧优先级单独显示
return '<span id="'+copyId+'" data-value="'+htmlEscape(value)+'">'+value+'</span>'
+ '<a href="javascript:void(0);" title="复制记录值" onclick="copyToClipboard(null, \'#'+copyId+'\')" style="padding-left:6px;"><i class=\"fa fa-copy\"></i></a>'
+ '<span class="mx-priority"> | '+row.MX+'</span>';
@ -349,10 +348,12 @@ $(document).ready(function(){
if(dnsconfig.remark == 1){
html += '<a href="javascript:setRemark(\''+row.RecordId+'\')" class="btn btn-info btn-xs">备注</a>&nbsp;&nbsp;';
}
if(row.Name === "@") var domain = "{$domainName}";
else var domain = row.Name + ".{$domainName}";
domain = domain.replace(/\*/g, 'www');
html += '<a href="http://' + domain + '" target="_blank" title="访问域名" class="btn btn-default btn-xs"><i class="fa fa-external-link"></i></a>';
if(row.Type == 'A' || row.Type == 'CNAME' || row.Type == 'AAAA' || row.Type == 'REDIRECT_URL' || row.Type == 'FORWARD_URL'){
if(row.Name === "@") var domain = "{$domainName}";
else var domain = row.Name + ".{$domainName}";
domain = domain.replace(/\*/g, 'www');
html += '<a href="http://' + domain + '" target="_blank" title="访问域名" class="btn btn-default btn-xs"><i class="fa fa-external-link"></i></a>';
}
return html;
}
},
@ -511,9 +512,7 @@ function setStatus(recordid, status){
}
function delItem(recordid) {
var row = $("#listTable").bootstrapTable('getRowByUniqueId', recordid);
var confirmobj = layer.confirm('确定要删除此解析记录吗?', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要删除此解析记录吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -531,8 +530,6 @@ function delItem(recordid) {
}
}
});
}, function(){
layer.close(confirmobj);
});
}
function setRemark(recordid) {
@ -587,9 +584,7 @@ function operation(action){
return;
}
var confirmobj = layer.confirm('确定要'+(action=='open'?'启用':(action=='pause'?'暂停':'删除'))+'所选记录吗?', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要'+(action=='open'?'启用':(action=='pause'?'暂停':'删除'))+'所选记录吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -607,8 +602,6 @@ function operation(action){
}
}
});
}, function(){
layer.close(confirmobj);
});
}
function batch_edit(records){

View File

@ -162,7 +162,7 @@
<form method="post">
<div id="error" style="display:none"></div>
<div id="success" style="display:none"></div>
{if $dbconfig!='1'}
<div class="form-group">
<div class="form-field">
<label>MySQL 数据库地址</label>
@ -194,6 +194,7 @@
<input type="text" name="mysql_prefix" value="dnsmgr_">
</div>
</div>
{/if}
<div class="form-group">
<div class="form-field">

View File

@ -54,14 +54,14 @@
<label class="col-sm-3 control-label no-padding-right">解析IP类型<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="同时开启IPv6&IPv4将会请求2次接口消耗双倍积分"><i class="fa fa-question-circle"></i></span></label>
<div class="col-sm-6">
<label class="checkbox-inline" v-for="option in iptypeList">
<input type="checkbox" name="ip_type" :value="option.value" v-model="set.ip_type_select" required> {{option.label}}
<input type="checkbox" name="ip_type" :value="option.value" v-model="set.ip_type_select" :disabled="option.value=='v6' && isXingpingcn" required> {{option.label}}<span v-if="option.value=='v6' && isXingpingcn" class="text-muted">(xingpingcn.top不支持)</span>
</label><br/>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">每线路解析数量<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="数量不要超过当前域名套餐允许的最大数量,否则会添加解析失败"><i class="fa fa-question-circle"></i></span></label>
<div class="col-sm-6">
<input type="text" name="recordnum" v-model="set.recordnum" placeholder="填写每线路解析数量" class="form-control" data-bv-integer="true" min="1" max="5" required>
<input type="text" name="recordnum" v-model="set.recordnum" placeholder="填写每线路解析数量" class="form-control" data-bv-integer="true" min="1" max="50" required>
</div>
</div>
<div class="form-group">
@ -110,6 +110,7 @@ new Vue({
el: '#app',
data: {
action: '{$action}',
optimize_ip_api: '{:config_get("optimize_ip_api", 0)}',
set: {
id: '',
remark: '',
@ -132,8 +133,18 @@ new Vue({
4:'EdgeOne'
},
},
computed: {
isXingpingcn: function() {
return this.optimize_ip_api == '2';
}
},
watch: {
'set.ip_type_select': function(val){
// 如果使用xingpingcn.top接口自动移除v6
if(this.isXingpingcn && val.includes('v6')){
this.set.ip_type_select = val.filter(v => v !== 'v6');
return;
}
this.set.ip_type = val.join(',');
}
},
@ -185,4 +196,4 @@ new Vue({
},
});
</script>
{/block}
{/block}

View File

@ -14,8 +14,9 @@
<div class="panel-heading"><h3 class="panel-title">使用说明</h3></div>
<div class="panel-body">
<p><li>不支持对CloudFlare里的域名添加优选必须使用其他DNS服务商。需开通Cloudflare for SaaS且域名使用CNAME的方式解析到CloudFlare。</li></p>
<p><li>数据接口:<a href="https://www.wetest.vip/" target="_blank" rel="noreferrer">wetest.vip</a> 数据接口支持CloudFlare、CloudFront、EdgeOne<a href="https://stock.hostmonit.com/" target="_blank" rel="noreferrer">HostMonit</a> 只支持CloudFlare。</li></p>
<p><li>数据接口:<a href="https://www.wetest.vip/" target="_blank" rel="noreferrer">wetest.vip</a> 数据接口支持CloudFlare、CloudFront、EdgeOne<a href="https://stock.hostmonit.com/" target="_blank" rel="noreferrer">HostMonit</a> 只支持CloudFlare<a href="https://github.com/xingpingcn/enhanced-FaaS-in-China" target="_blank" rel="noreferrer">xingpingcn.top</a> 只支持CloudFlare免费、无需密钥</li></p>
<p><li>接口密钥默认o1zrmHAF为免费KEY可永久免费使用。</li></p>
<p><li>代理地址:如 https://ghfast.top/https://raw.githubusercontent.com/ ,留空则直接访问 https://raw.githubusercontent.com/。</li></p>
<p><li>自动更新:可查看<a href="/system/cronset">计划任务设置</a></p>
</div>
</div>
@ -24,19 +25,23 @@
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">数据接口设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form" id="apiSettingForm">
<div class="form-group">
<label class="col-sm-3 control-label">数据接口</label>
<div class="col-sm-9"><select class="form-control" name="optimize_ip_api" default="{:config_get('optimize_ip_api')}"><option value="0">wetest.vip</option><option value="1">HostMonit</option></select></div>
<div class="col-sm-9"><select class="form-control" name="optimize_ip_api" id="optimize_ip_api" default="{:config_get('optimize_ip_api')}"><option value="0">wetest.vip</option><option value="1">HostMonit</option><option value="2">xingpingcn.top</option></select></div>
</div>
<div class="form-group">
<div class="form-group" id="keyGroup">
<label class="col-sm-3 control-label">接口密钥</label>
<div class="col-sm-9"><input type="text" name="optimize_ip_key" value="{:config_get('optimize_ip_key', 'o1zrmHAF')}" class="form-control"/></div>
</div>
<div class="form-group" id="proxyGroup" style="display:none;">
<label class="col-sm-3 control-label">代理地址</label>
<div class="col-sm-9"><input type="text" name="optimize_ip_proxy" value="{:config_get('optimize_ip_proxy', '')}" class="form-control" placeholder="留空则直接访问GitHub"/></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
<a href="javascript:queryapi()" class="btn btn-default btn-block">查询积分</a>
<a href="javascript:queryapi()" class="btn btn-default btn-block" id="queryBtn">查询积分</a>
</div>
</div>
</form>
@ -69,6 +74,25 @@ var items = $("select[default]");
for (i = 0; i < items.length; i++) {
$(items[i]).val($(items[i]).attr("default")||0);
}
// 切换接口时显示/隐藏对应设置项
function toggleApiSettings(){
var api = $("#optimize_ip_api").val();
if(api == '2'){
$("#keyGroup").hide();
$("#proxyGroup").show();
$("#queryBtn").hide();
}else{
$("#keyGroup").show();
$("#proxyGroup").hide();
$("#queryBtn").show();
}
}
$("#optimize_ip_api").change(function(){
toggleApiSettings();
});
// 页面加载时初始化
toggleApiSettings();
$('[data-toggle="tooltip"]').tooltip();
function saveSetting(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
@ -118,4 +142,4 @@ function queryapi(){
});
}
</script>
{/block}
{/block}

View File

@ -121,6 +121,10 @@
<label class="col-sm-3 control-label">Webhook地址</label>
<div class="col-sm-9"><input type="text" name="webhook_url" value="{:config_get('webhook_url')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">@用户手机号</label>
<div class="col-sm-9"><input type="text" name="webhook_user" value="{:config_get('webhook_user')}" class="form-control" placeholder="非必填,可填写用户的手机号,@全体填写all"/></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
@ -130,7 +134,9 @@
</form>
</div>
<div class="panel-footer">
仅支持填写企业微信、钉钉、飞书群机器人的Webhook地址
仅支持填写企业微信、钉钉、飞书群机器人的Webhook地址<br/>
认证方式可以选自定义关键词“DNS”或IP白名单。<br/>
@用户不支持企业微信,飞书用户手机号需要填写<a href="https://open.feishu.cn/document/home/user-identity-introduction/open-id" target="_blank" rel="noreferrer">用户ID</a>
</div>
</div>
</div>

View File

@ -279,9 +279,7 @@ function setStatus(id,status) {
});
}
function delItem(id) {
var confirmobj = layer.confirm('确定要删除此用户吗?', {
btn: ['确定','取消']
}, function(){
layer.confirm('确定要删除此用户吗?', {title: '提示', icon: 0}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
@ -298,8 +296,6 @@ function delItem(id) {
}
}
});
}, function(){
layer.close(confirmobj);
});
}
var CreatePassword = function (len)

79
composer.lock generated
View File

@ -8,16 +8,16 @@
"packages": [
{
"name": "cccyun/php-whois",
"version": "1.2",
"version": "1.3",
"source": {
"type": "git",
"url": "https://github.com/netcccyun/php-whois.git",
"reference": "c631f1c5e26e7150501a14cd25a2380f8a077ca1"
"reference": "f02627ba0bef005aa9e336d63541f9fd288675b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/netcccyun/php-whois/zipball/c631f1c5e26e7150501a14cd25a2380f8a077ca1",
"reference": "c631f1c5e26e7150501a14cd25a2380f8a077ca1",
"url": "https://api.github.com/repos/netcccyun/php-whois/zipball/f02627ba0bef005aa9e336d63541f9fd288675b5",
"reference": "f02627ba0bef005aa9e336d63541f9fd288675b5",
"shasum": ""
},
"require": {
@ -62,9 +62,9 @@
"црщшы"
],
"support": {
"source": "https://github.com/netcccyun/php-whois/tree/1.2"
"source": "https://github.com/netcccyun/php-whois/tree/1.3"
},
"time": "2025-06-25T06:54:23+00:00"
"time": "2026-02-12T05:56:18+00:00"
},
{
"name": "cccyun/think-captcha",
@ -445,16 +445,16 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v7.0.1",
"version": "v7.0.2",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "360ae911ce62e25e11249f6140fa58939f556ebe"
"reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/360ae911ce62e25e11249f6140fa58939f556ebe",
"reference": "360ae911ce62e25e11249f6140fa58939f556ebe",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
"reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
"shasum": ""
},
"require": {
@ -515,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/v7.0.1"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.0.2"
},
"funding": [
{
@ -523,7 +523,7 @@
"type": "github"
}
],
"time": "2025-11-25T07:18:09+00:00"
"time": "2026-01-09T18:02:33+00:00"
},
{
"name": "psr/container",
@ -687,16 +687,16 @@
},
{
"name": "psr/http-message",
"version": "1.1",
"version": "2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": ""
},
"require": {
@ -705,7 +705,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
"dev-master": "2.0.x-dev"
}
},
"autoload": {
@ -720,7 +720,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
@ -734,9 +734,9 @@
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/1.1"
"source": "https://github.com/php-fig/http-message/tree/2.0"
},
"time": "2023-04-04T09:50:52+00:00"
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "psr/log",
@ -1528,16 +1528,16 @@
},
{
"name": "topthink/framework",
"version": "v8.1.3",
"version": "v8.1.4",
"source": {
"type": "git",
"url": "https://github.com/top-think/framework.git",
"reference": "e4207e98b66f92d26097ed6efd535930cba90e8f"
"reference": "8e7b2b2364047cbf71a38c4e397a9ca0d4ef2b01"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/framework/zipball/e4207e98b66f92d26097ed6efd535930cba90e8f",
"reference": "e4207e98b66f92d26097ed6efd535930cba90e8f",
"url": "https://api.github.com/repos/top-think/framework/zipball/8e7b2b2364047cbf71a38c4e397a9ca0d4ef2b01",
"reference": "8e7b2b2364047cbf71a38c4e397a9ca0d4ef2b01",
"shasum": ""
},
"require": {
@ -1545,7 +1545,7 @@
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=8.0.0",
"psr/http-message": "^1.0",
"psr/http-message": "^1.0|^2.0",
"psr/log": "^1.0|^2.0|^3.0",
"psr/simple-cache": "^1.0|^2.0|^3.0",
"topthink/think-container": "^3.0",
@ -1554,6 +1554,7 @@
"topthink/think-validate": "^3.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.92",
"guzzlehttp/psr7": "^2.1.0",
"mikey179/vfsstream": "^1.6",
"mockery/mockery": "^1.2",
@ -1589,9 +1590,9 @@
],
"support": {
"issues": "https://github.com/top-think/framework/issues",
"source": "https://github.com/top-think/framework/tree/v8.1.3"
"source": "https://github.com/top-think/framework/tree/v8.1.4"
},
"time": "2025-07-14T03:48:44+00:00"
"time": "2026-01-15T02:45:10+00:00"
},
{
"name": "topthink/think-container",
@ -1641,16 +1642,16 @@
},
{
"name": "topthink/think-helper",
"version": "v3.1.11",
"version": "v3.1.12",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-helper.git",
"reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c"
"reference": "fe277121112a8f1c872e169a733ca80bb11c4acb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/think-helper/zipball/1d6ada9b9f3130046bf6922fe1bd159c8d88a33c",
"reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c",
"url": "https://api.github.com/repos/top-think/think-helper/zipball/fe277121112a8f1c872e169a733ca80bb11c4acb",
"reference": "fe277121112a8f1c872e169a733ca80bb11c4acb",
"shasum": ""
},
"require": {
@ -1681,22 +1682,22 @@
"description": "The ThinkPHP6 Helper Package",
"support": {
"issues": "https://github.com/top-think/think-helper/issues",
"source": "https://github.com/top-think/think-helper/tree/v3.1.11"
"source": "https://github.com/top-think/think-helper/tree/v3.1.12"
},
"time": "2025-04-07T06:55:59+00:00"
"time": "2025-12-26T09:58:29+00:00"
},
{
"name": "topthink/think-orm",
"version": "v4.0.50",
"version": "v4.0.51",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-orm.git",
"reference": "ddae72d5ff4d953d3d8cc526fd9c50e8862ce2cc"
"reference": "46abe2f824eb3bcb117d4c0ce93b203b592b79f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/think-orm/zipball/ddae72d5ff4d953d3d8cc526fd9c50e8862ce2cc",
"reference": "ddae72d5ff4d953d3d8cc526fd9c50e8862ce2cc",
"url": "https://api.github.com/repos/top-think/think-orm/zipball/46abe2f824eb3bcb117d4c0ce93b203b592b79f7",
"reference": "46abe2f824eb3bcb117d4c0ce93b203b592b79f7",
"shasum": ""
},
"require": {
@ -1741,9 +1742,9 @@
],
"support": {
"issues": "https://github.com/top-think/think-orm/issues",
"source": "https://github.com/top-think/think-orm/tree/v4.0.50"
"source": "https://github.com/top-think/think-orm/tree/v4.0.51"
},
"time": "2025-08-26T05:32:22+00:00"
"time": "2025-12-18T13:11:52+00:00"
},
{
"name": "topthink/think-template",

View File

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

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="812" height="812">
<rect x="0" y="0" width="812" height="812" rx="80" ry="80" fill="#333333"/>
<path d="M0,0 L136,0 L138,23 L141,47 L146,91 L152,146 L155,169 L157,188 L161,222 L163,243 L167,275 L169,294 L173,329 L176,355 L178,372 L178,377 L360,377 L354,399 L342,439 L333,470 L324,501 L321,509 L302,509 L307,504 L313,497 L313,494 L296,481 L293,481 L286,489 L277,494 L270,496 L261,496 L253,493 L248,487 L246,481 L246,474 L247,473 L276,470 L296,464 L307,458 L314,451 L318,443 L320,430 L318,420 L313,412 L306,406 L294,401 L290,400 L267,400 L254,404 L243,410 L233,419 L223,434 L217,450 L215,464 L215,478 L217,490 L222,502 L226,507 L226,509 L187,509 L193,501 L199,492 L199,488 L179,479 L176,478 L169,488 L164,493 L157,496 L150,496 L142,493 L138,488 L136,480 L138,464 L142,445 L146,435 L151,429 L157,426 L162,425 L169,425 L176,428 L181,436 L182,439 L187,438 L208,430 L207,422 L201,412 L193,406 L182,401 L175,400 L155,400 L143,404 L132,410 L122,419 L115,430 L110,440 L107,450 L105,468 L106,488 L111,500 L116,509 L86,509 L84,496 L81,460 L78,432 L74,385 L-27,385 L-22,373 L-7,342 L1,324 L9,308 L14,297 L67,297 L66,293 L62,236 L60,213 L57,169 L52,104 L52,95 L45,95 L43,101 L27,136 L11,171 L-7,210 L-26,251 L-42,285 L-50,304 L-60,326 L-68,342 L-76,359 L-84,375 L-96,400 L-114,437 L-125,460 L-137,485 L-148,508 L-149,509 L-261,509 L-259,503 L-245,475 L-235,456 L-212,411 L-202,392 L-189,366 L-179,347 L-159,308 L-145,281 L-137,266 L-129,250 L-105,203 L-91,176 L-82,159 L-74,143 L-51,98 L-34,65 L-19,36 L-1,1 Z " fill="#FEFEFE" transform="translate(372,151)"/>
<path d="M0,0 L11,0 L17,3 L19,6 L19,15 L14,21 L4,25 L-14,28 L-20,28 L-18,17 L-13,9 L-5,2 Z " fill="#FBFBFB" transform="translate(642,575)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

View File

@ -47,7 +47,8 @@ Route::group(function () {
Route::get('/log', 'user/log');
Route::post('/account/data', 'domain/account_data');
Route::post('/account/op', 'domain/account_op');
Route::post('/account/:action', 'domain/account_op');
Route::get('/account/:action', 'domain/account_add');
Route::get('/account', 'domain/account');
Route::any('/domain/expirenotice', 'domain/expire_notice');