diff --git a/.example.env b/.example.env index 95e1469..037c165 100644 --- a/.example.env +++ b/.example.env @@ -1,18 +1,18 @@ -APP_DEBUG = false - -[APP] -DEFAULT_TIMEZONE = Asia/Shanghai - -[DATABASE] -TYPE = mysql -HOSTNAME = {dbhost} -DATABASE = {dbname} -USERNAME = {dbuser} -PASSWORD = {dbpwd} -HOSTPORT = {dbport} -CHARSET = utf8mb4 -PREFIX = {dbprefix} -DEBUG = false - -[LANG] +APP_DEBUG = false + +[APP] +DEFAULT_TIMEZONE = Asia/Shanghai + +[DATABASE] +TYPE = mysql +HOSTNAME = {dbhost} +DATABASE = {dbname} +USERNAME = {dbuser} +PASSWORD = {dbpwd} +HOSTPORT = {dbport} +CHARSET = utf8mb4 +PREFIX = {dbprefix} +DEBUG = false + +[LANG] default_lang = zh-cn \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/README.md b/README.md index 9f895cd..8cf8abd 100644 --- a/README.md +++ b/README.md @@ -1,209 +1,209 @@ -# 彩虹聚合DNS管理系统 - -
- -[![GitHub stars](https://img.shields.io/github/stars/netcccyun/dnsmgr?style=flat)](https://github.com/netcccyun/dnsmgr/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/netcccyun/dnsmgr?style=flat)](https://github.com/netcccyun/dnsmgr/forks) -[![Docker Pulls](https://img.shields.io/docker/pulls/netcccyun/dnsmgr?style=flat)](https://hub.docker.com/r/netcccyun/dnsmgr) -[![GitHub release (latest by date)](https://img.shields.io/github/v/release/netcccyun/dnsmgr)](https://github.com/netcccyun/dnsmgr/releases) -[![GitHub last commit](https://img.shields.io/github/last-commit/netcccyun/dnsmgr)](https://github.com/netcccyun/dnsmgr/commits/main) - -
- -彩虹聚合DNS管理系统 是一款基于ThinkPHP开发的网站程序,可实现在单一网站内管理多个平台的域名解析,目前已支持的域名解析平台有:阿里云、腾讯云、华为云、百度云、西部数码、火山引擎、DNSLA、CloudFlare、Namesilo、PowerDNS - -## 功能特性 - -- 多用户管理,可为每个用户可分配不同的域名解析权限; -- 提供API接口,可获取域名单独的登录链接,方便各种IDC系统对接; -- 容灾切换功能,支持ping、tcp、http(s)检测协议并自动暂停/修改域名解析,并支持发送通知; -- 定时切换功能,设置在指定时间/周期,自动修改/开启/暂停/删除域名解析; -- CF优选IP功能,支持获取最新的Cloudflare优选IP,并自动更新到解析记录; -- SSL证书申请与自动部署功能,支持从Let's Encrypt等渠道申请SSL证书,并自动部署到各种面板、云服务商、服务器等; -- 支持邮件、微信公众号、Telegram、钉钉、飞书、企业微信等多种通知渠道。 - -## 部署方式 - -### 自部署 - -可以使用宝塔、Kangle等任意支持PHP-MySQL的环境部署 - -* 从[Release](https://github.com/netcccyun/dnsmgr/releases)页面下载安装包 - -* 运行环境要求PHP8.0+,MySQL5.6+ - -* 设置网站运行目录为`public` - -* 设置伪静态为`ThinkPHP` - -* 如果是下载的Source code包,还需Composer安装依赖(Release页面下载的安装包不需要) - - ``` - composer install --no-dev - ``` - -* 访问网站,会自动跳转到安装页面,根据提示安装完成 - -* 访问首页登录控制面板 - -* 后续更新方式:重新下载安装包上传覆盖即可 - -##### 伪静态规则 - -* Nginx - -``` -location ~* (runtime|application)/ { - return 403; -} -location / { - if (!-e $request_filename) { - rewrite ^(.*)$ /index.php?s=$1 last; break; - } -} -``` - -* Apache - -``` - - Options +FollowSymlinks -Multiviews - RewriteEngine On - - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_FILENAME} !-f - RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] - -``` - -### Docker 部署 - -首先需要安装Docker,然后执行以下命令拉取镜像并启动(启动后监听8081端口): - -``` -docker run --name dnsmgr -dit -p 8081:80 -v /var/dnsmgr:/app/www netcccyun/dnsmgr -``` - -访问并安装好后如果容灾切换未自动启动,重启容器即可: - -``` -docker restart dnsmgr -``` - -从国内镜像地址拉取: - -``` -docker pull swr.cn-east-3.myhuaweicloud.com/netcccyun/dnsmgr:latest -``` - -### docker-compose 部署 - -``` -services: - dnsmgr-web: - container_name: dnsmgr-web - stdin_open: true - tty: true - ports: - - 8081:80 - volumes: - - ./web:/app/www - image: netcccyun/dnsmgr - depends_on: - - dnsmgr-mysql - networks: - - dnsmgr-network - - dnsmgr-mysql: - container_name: dnsmgr-mysql - restart: always - ports: - - 3306:3306 - volumes: - - ./mysql/conf/my.cnf:/etc/mysql/my.cnf - - ./mysql/logs:/logs - - ./mysql/data:/var/lib/mysql - environment: - - MYSQL_ROOT_PASSWORD=123456 - - TZ=Asia/Shanghai - image: mysql:5.7 - networks: - - dnsmgr-network - -networks: - dnsmgr-network: - driver: bridge -``` - -在运行之前请创建好目录 - -``` -mkdir -p ./web -mkdir -p ./mysql/conf -mkdir -p ./mysql/logs -mkdir -p ./mysql/data - -vim mysql/conf/my.cnf -[mysqld] -sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION -``` - -登陆mysql容器创建数据库 - -``` -docker exec -it dnsmgr-mysql /bin/bash -mysql -uroot -p123456 -create database dnsmgr; -``` - -在install界面链接IP填写dnsmgr-mysql - -## 演示截图 - -添加域名账户 - -![](https://p0.meituan.net/csc/090508cdc7aaabd185ba9c76a8c099f9283946.png) - -域名管理列表 - -![](https://p0.meituan.net/csc/60bf3f607d40f30f152ad1f6ee3be098357839.png) - -域名DNS解析管理,支持解析批量操作 - -![](https://p0.meituan.net/csc/f99c599d4febced404c88672dd50d62c212895.png) - -用户管理添加用户,支持为用户开启API接口 - -![](https://p0.meituan.net/csc/d1bd90bedca9b6cbc5da40286bdb5cd5228438.png) - -CF优选IP功能,添加优选IP任务 - -![](https://p1.meituan.net/csc/da70c76753aee4bce044d16fadd56e5f217660.png) - -SSL证书申请功能 - -![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154857.png) - -![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154652.png?a) - -SSL证书自动部署功能 - -![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154702.png) - -![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154804.png) - -## 支持与反馈 - -🌐 作者信息:消失的彩虹海(https://blog.cccyun.cn) - -⭐ 如果您觉得本项目对您有帮助,欢迎给项目点个 Star - -🤝 捐赠: - - - -### 其他推荐 - -- [彩虹云主机 - 免备案CDN/虚拟主机](https://www.cccyun.net/) -- [小白云高防云服务器](https://www.xiaobaiyun.cn/aff/GMLPMFOV) - +# 彩虹聚合DNS管理系统 + +
+ +[![GitHub stars](https://img.shields.io/github/stars/netcccyun/dnsmgr?style=flat)](https://github.com/netcccyun/dnsmgr/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/netcccyun/dnsmgr?style=flat)](https://github.com/netcccyun/dnsmgr/forks) +[![Docker Pulls](https://img.shields.io/docker/pulls/netcccyun/dnsmgr?style=flat)](https://hub.docker.com/r/netcccyun/dnsmgr) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/netcccyun/dnsmgr)](https://github.com/netcccyun/dnsmgr/releases) +[![GitHub last commit](https://img.shields.io/github/last-commit/netcccyun/dnsmgr)](https://github.com/netcccyun/dnsmgr/commits/main) + +
+ +彩虹聚合DNS管理系统 是一款基于ThinkPHP开发的网站程序,可实现在单一网站内管理多个平台的域名解析,目前已支持的域名解析平台有:阿里云、腾讯云、华为云、百度云、西部数码、火山引擎、DNSLA、CloudFlare、Namesilo、PowerDNS + +## 功能特性 + +- 多用户管理,可为每个用户可分配不同的域名解析权限; +- 提供API接口,可获取域名单独的登录链接,方便各种IDC系统对接; +- 容灾切换功能,支持ping、tcp、http(s)检测协议并自动暂停/修改域名解析,并支持发送通知; +- 定时切换功能,设置在指定时间/周期,自动修改/开启/暂停/删除域名解析; +- CF优选IP功能,支持获取最新的Cloudflare优选IP,并自动更新到解析记录; +- SSL证书申请与自动部署功能,支持从Let's Encrypt等渠道申请SSL证书,并自动部署到各种面板、云服务商、服务器等; +- 支持邮件、微信公众号、Telegram、钉钉、飞书、企业微信等多种通知渠道。 + +## 部署方式 + +### 自部署 + +可以使用宝塔、Kangle等任意支持PHP-MySQL的环境部署 + +* 从[Release](https://github.com/netcccyun/dnsmgr/releases)页面下载安装包 + +* 运行环境要求PHP8.0+,MySQL5.6+ + +* 设置网站运行目录为`public` + +* 设置伪静态为`ThinkPHP` + +* 如果是下载的Source code包,还需Composer安装依赖(Release页面下载的安装包不需要) + + ``` + composer install --no-dev + ``` + +* 访问网站,会自动跳转到安装页面,根据提示安装完成 + +* 访问首页登录控制面板 + +* 后续更新方式:重新下载安装包上传覆盖即可 + +##### 伪静态规则 + +* Nginx + +``` +location ~* (runtime|application)/ { + return 403; +} +location / { + if (!-e $request_filename) { + rewrite ^(.*)$ /index.php?s=$1 last; break; + } +} +``` + +* Apache + +``` + + Options +FollowSymlinks -Multiviews + RewriteEngine On + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] + +``` + +### Docker 部署 + +首先需要安装Docker,然后执行以下命令拉取镜像并启动(启动后监听8081端口): + +``` +docker run --name dnsmgr -dit -p 8081:80 -v /var/dnsmgr:/app/www netcccyun/dnsmgr +``` + +访问并安装好后如果容灾切换未自动启动,重启容器即可: + +``` +docker restart dnsmgr +``` + +从国内镜像地址拉取: + +``` +docker pull swr.cn-east-3.myhuaweicloud.com/netcccyun/dnsmgr:latest +``` + +### docker-compose 部署 + +``` +services: + dnsmgr-web: + container_name: dnsmgr-web + stdin_open: true + tty: true + ports: + - 8081:80 + volumes: + - ./web:/app/www + image: netcccyun/dnsmgr + depends_on: + - dnsmgr-mysql + networks: + - dnsmgr-network + + dnsmgr-mysql: + container_name: dnsmgr-mysql + restart: always + ports: + - 3306:3306 + volumes: + - ./mysql/conf/my.cnf:/etc/mysql/my.cnf + - ./mysql/logs:/logs + - ./mysql/data:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD=123456 + - TZ=Asia/Shanghai + image: mysql:5.7 + networks: + - dnsmgr-network + +networks: + dnsmgr-network: + driver: bridge +``` + +在运行之前请创建好目录 + +``` +mkdir -p ./web +mkdir -p ./mysql/conf +mkdir -p ./mysql/logs +mkdir -p ./mysql/data + +vim mysql/conf/my.cnf +[mysqld] +sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION +``` + +登陆mysql容器创建数据库 + +``` +docker exec -it dnsmgr-mysql /bin/bash +mysql -uroot -p123456 +create database dnsmgr; +``` + +在install界面链接IP填写dnsmgr-mysql + +## 演示截图 + +添加域名账户 + +![](https://p0.meituan.net/csc/090508cdc7aaabd185ba9c76a8c099f9283946.png) + +域名管理列表 + +![](https://p0.meituan.net/csc/60bf3f607d40f30f152ad1f6ee3be098357839.png) + +域名DNS解析管理,支持解析批量操作 + +![](https://p0.meituan.net/csc/f99c599d4febced404c88672dd50d62c212895.png) + +用户管理添加用户,支持为用户开启API接口 + +![](https://p0.meituan.net/csc/d1bd90bedca9b6cbc5da40286bdb5cd5228438.png) + +CF优选IP功能,添加优选IP任务 + +![](https://p1.meituan.net/csc/da70c76753aee4bce044d16fadd56e5f217660.png) + +SSL证书申请功能 + +![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154857.png) + +![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154652.png?a) + +SSL证书自动部署功能 + +![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154702.png) + +![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154804.png) + +## 支持与反馈 + +🌐 作者信息:消失的彩虹海(https://blog.cccyun.cn) + +⭐ 如果您觉得本项目对您有帮助,欢迎给项目点个 Star + +🤝 捐赠: + + + +### 其他推荐 + +- [彩虹云主机 - 免备案CDN/虚拟主机](https://www.cccyun.net/) +- [小白云高防云服务器](https://www.xiaobaiyun.cn/aff/GMLPMFOV) + diff --git a/app/command/Certtask.php b/app/command/Certtask.php index d75f8e4..7023698 100644 --- a/app/command/Certtask.php +++ b/app/command/Certtask.php @@ -1,42 +1,42 @@ -setName('certtask') - ->setDescription('SSL证书续签与部署、域名到期提醒、定时切换解析、CF优选IP更新'); - } - - protected function execute(Input $input, Output $output) - { - $res = Db::name('config')->cache('configs', 0)->column('value', 'key'); - Config::set($res, 'sys'); - - (new ScheduleService())->execute(); - $res = (new OptimizeService())->execute(); - if (!$res) { - (new CertTaskService())->execute(); - (new ExpireNoticeService())->task(); - } - echo 'done'.PHP_EOL; - } -} +setName('certtask') + ->setDescription('SSL证书续签与部署、域名到期提醒、定时切换解析、CF优选IP更新'); + } + + protected function execute(Input $input, Output $output) + { + $res = Db::name('config')->cache('configs', 0)->column('value', 'key'); + Config::set($res, 'sys'); + + (new ScheduleService())->execute(); + $res = (new OptimizeService())->execute(); + if (!$res) { + (new CertTaskService())->execute(); + (new ExpireNoticeService())->task(); + } + echo 'done'.PHP_EOL; + } +} diff --git a/app/command/Dmtask.php b/app/command/Dmtask.php index f69eb53..0c46140 100644 --- a/app/command/Dmtask.php +++ b/app/command/Dmtask.php @@ -1,82 +1,82 @@ -setName('dmtask') - ->setDescription('容灾切换任务'); - } - - protected function execute(Input $input, Output $output) - { - $res = Db::name('config')->cache('configs', 0)->column('value', 'key'); - Config::set($res, 'sys'); - - config_set('run_error', ''); - if (!extension_loaded('swoole')) { - $output->writeln('[Error] 未安装Swoole扩展'); - config_set('run_error', '未安装Swoole扩展'); - return; - } - try { - $output->writeln('进程启动成功.'); - $this->runtask(); - } catch (Exception $e) { - $output->writeln('[Error] ' . $e->getMessage()); - config_set('run_error', $e->getMessage()); - } - } - - private function runtask() - { - \Co::set(['hook_flags' => SWOOLE_HOOK_ALL]); - \Co\run(function () { - $date = date("Ymd"); - $count = config_get('run_count', null, true) ?? 0; - while (true) { - sleep(1); - if ($date != date("Ymd")) { - $count = 0; - $date = date("Ymd"); - } - - $rows = Db::name('dmtask')->where('checknexttime', '<=', time())->where('active', 1)->order('id', 'ASC')->select(); - foreach ($rows as $row) { - \go(function () use ($row) { - try { - (new TaskRunner())->execute($row); - } catch (\Swoole\ExitException $e) { - echo $e->getStatus() . "\n"; - } catch (Exception $e) { - echo $e->__toString() . "\n"; - } - }); - Db::name('dmtask')->where('id', $row['id'])->update([ - 'checktime' => time(), - 'checknexttime' => time() + $row['frequency'] - ]); - $count++; - } - - config_set('run_time', date("Y-m-d H:i:s")); - config_set('run_count', $count); - } - }); - } -} +setName('dmtask') + ->setDescription('容灾切换任务'); + } + + protected function execute(Input $input, Output $output) + { + $res = Db::name('config')->cache('configs', 0)->column('value', 'key'); + Config::set($res, 'sys'); + + config_set('run_error', ''); + if (!extension_loaded('swoole')) { + $output->writeln('[Error] 未安装Swoole扩展'); + config_set('run_error', '未安装Swoole扩展'); + return; + } + try { + $output->writeln('进程启动成功.'); + $this->runtask(); + } catch (Exception $e) { + $output->writeln('[Error] ' . $e->getMessage()); + config_set('run_error', $e->getMessage()); + } + } + + private function runtask() + { + \Co::set(['hook_flags' => SWOOLE_HOOK_ALL]); + \Co\run(function () { + $date = date("Ymd"); + $count = config_get('run_count', null, true) ?? 0; + while (true) { + sleep(1); + if ($date != date("Ymd")) { + $count = 0; + $date = date("Ymd"); + } + + $rows = Db::name('dmtask')->where('checknexttime', '<=', time())->where('active', 1)->order('id', 'ASC')->select(); + foreach ($rows as $row) { + \go(function () use ($row) { + try { + (new TaskRunner())->execute($row); + } catch (\Swoole\ExitException $e) { + echo $e->getStatus() . "\n"; + } catch (Exception $e) { + echo $e->__toString() . "\n"; + } + }); + Db::name('dmtask')->where('id', $row['id'])->update([ + 'checktime' => time(), + 'checknexttime' => time() + $row['frequency'] + ]); + $count++; + } + + config_set('run_time', date("Y-m-d H:i:s")); + config_set('run_count', $count); + } + }); + } +} diff --git a/app/command/Reset.php b/app/command/Reset.php index 019387f..d968d62 100644 --- a/app/command/Reset.php +++ b/app/command/Reset.php @@ -1,47 +1,47 @@ -setName('reset') - ->addArgument('type', Argument::REQUIRED, '操作类型,pwd:重置密码,totp:关闭TOTP') - ->addArgument('username', Argument::REQUIRED, '用户名') - ->addArgument('password', Argument::OPTIONAL, '密码') - ->setDescription('重置密码'); - } - - protected function execute(Input $input, Output $output) - { - $type = trim($input->getArgument('type')); - $username = trim($input->getArgument('username')); - $user = Db::name('user')->where('username', $username)->find(); - if (!$user) { - $output->writeln('用户 ' . $username . ' 不存在'); - return; - } - if ($type == 'pwd') { - $password = $input->getArgument('password'); - if (empty($password)) $password = '123456'; - Db::name('user')->where('id', $user['id'])->update(['password' => password_hash($password, PASSWORD_DEFAULT)]); - $output->writeln('用户 ' . $username . ' 密码重置成功'); - } elseif ($type == 'totp') { - Db::name('user')->where('id', $user['id'])->update(['totp_open' => 0, 'totp_secret' => null]); - $output->writeln('用户 ' . $username . ' TOTP关闭成功'); - } - } -} +setName('reset') + ->addArgument('type', Argument::REQUIRED, '操作类型,pwd:重置密码,totp:关闭TOTP') + ->addArgument('username', Argument::REQUIRED, '用户名') + ->addArgument('password', Argument::OPTIONAL, '密码') + ->setDescription('重置密码'); + } + + protected function execute(Input $input, Output $output) + { + $type = trim($input->getArgument('type')); + $username = trim($input->getArgument('username')); + $user = Db::name('user')->where('username', $username)->find(); + if (!$user) { + $output->writeln('用户 ' . $username . ' 不存在'); + return; + } + if ($type == 'pwd') { + $password = $input->getArgument('password'); + if (empty($password)) $password = '123456'; + Db::name('user')->where('id', $user['id'])->update(['password' => password_hash($password, PASSWORD_DEFAULT)]); + $output->writeln('用户 ' . $username . ' 密码重置成功'); + } elseif ($type == 'totp') { + Db::name('user')->where('id', $user['id'])->update(['totp_open' => 0, 'totp_secret' => null]); + $output->writeln('用户 ' . $username . ' TOTP关闭成功'); + } + } +} diff --git a/app/controller/Cert.php b/app/controller/Cert.php index 28f9437..1693395 100644 --- a/app/controller/Cert.php +++ b/app/controller/Cert.php @@ -1,962 +1,962 @@ -alert('error', '无权限'); - return view(); - } - - public function deployaccount() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - return view(); - } - - public function account_data() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $deploy = input('get.deploy/d', 0); - $kw = $this->request->post('kw', null, 'trim'); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - - $select = Db::name('cert_account')->where('deploy', $deploy); - if (!empty($kw)) { - $select->whereLike('name|remark', '%' . $kw . '%')->whereOr('id', $kw); - } - $total = $select->count(); - $rows = $select->order('id', 'desc')->limit($offset, $limit)->select(); - - $list = []; - foreach ($rows as $row) { - if ($deploy == 1) { - if (!empty($row['type']) && isset(DeployHelper::$deploy_config[$row['type']])) { - $row['typename'] = DeployHelper::$deploy_config[$row['type']]['name']; - $row['icon'] = DeployHelper::$deploy_config[$row['type']]['icon']; - } - } else { - if (!empty($row['type']) && isset(CertHelper::$cert_config[$row['type']])) { - $row['typename'] = CertHelper::$cert_config[$row['type']]['name']; - $row['icon'] = CertHelper::$cert_config[$row['type']]['icon']; - } - } - $list[] = $row; - } - - return json(['total' => $total, 'rows' => $list]); - } - - public function account_op() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - $deploy = input('post.deploy/d', 0); - $title = $deploy == 1 ? '自动部署账户' : 'SSL证书账户'; - - if ($action == 'add') { - $type = input('post.type'); - $name = input('post.name', null, 'trim'); - $config = input('post.config', null, 'trim'); - $remark = input('post.remark', null, 'trim'); - if ($type == 'local') $name = '复制到本机'; - if (empty($name) || empty($config)) return json(['code' => -1, 'msg' => '必填参数不能为空']); - if (Db::name('cert_account')->where('type', $type)->where('config', $config)->find()) { - return json(['code' => -1, 'msg' => $title . '已存在']); - } - Db::startTrans(); - $id = Db::name('cert_account')->insertGetId([ - 'type' => $type, - 'name' => $name, - 'config' => $config, - 'remark' => $remark, - 'deploy' => $deploy, - 'addtime' => date('Y-m-d H:i:s'), - ]); - try { - $this->checkAccount($id, $type, $deploy); - Db::commit(); - return json(['code' => 0, 'msg' => '添加' . $title . '成功!']); - } catch (Exception $e) { - Db::rollback(); - return json(['code' => -1, 'msg' => $e->getMessage()]); - } - } elseif ($action == 'edit') { - $id = input('post.id/d'); - $row = Db::name('cert_account')->where('id', $id)->find(); - if (!$row) return json(['code' => -1, 'msg' => $title . '不存在']); - $type = input('post.type'); - $name = input('post.name', null, 'trim'); - $config = input('post.config', null, 'trim'); - $remark = input('post.remark', null, 'trim'); - if ($type == 'local') $name = '复制到本机'; - if (empty($name) || empty($config)) return json(['code' => -1, 'msg' => '必填参数不能为空']); - if (Db::name('cert_account')->where('type', $type)->where('config', $config)->where('id', '<>', $id)->find()) { - return json(['code' => -1, 'msg' => $title . '已存在']); - } - Db::startTrans(); - Db::name('cert_account')->where('id', $id)->update([ - 'type' => $type, - 'name' => $name, - 'config' => $config, - 'remark' => $remark, - ]); - try { - $this->checkAccount($id, $type, $deploy); - Db::commit(); - return json(['code' => 0, 'msg' => '修改' . $title . '成功!']); - } catch (Exception $e) { - Db::rollback(); - return json(['code' => -1, 'msg' => $e->getMessage()]); - } - } elseif ($action == 'del') { - $id = input('post.id/d'); - if ($deploy == 0) { - $dcount = DB::name('cert_order')->where('aid', $id)->count(); - if ($dcount > 0) return json(['code' => -1, 'msg' => '该' . $title . '下存在证书订单,无法删除']); - } else { - $dcount = DB::name('cert_deploy')->where('aid', $id)->count(); - if ($dcount > 0) return json(['code' => -1, 'msg' => '该' . $title . '下存在自动部署任务,无法删除']); - } - Db::name('cert_account')->where('id', $id)->delete(); - return json(['code' => 0]); - } - return json(['code' => -3]); - } - - public function account_form() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - $deploy = input('get.deploy/d', 0); - $title = $deploy == 1 ? '自动部署账户' : 'SSL证书账户'; - - $account = null; - if ($action == 'edit') { - $id = input('get.id/d'); - $account = Db::name('cert_account')->where('id', $id)->find(); - if (empty($account)) return $this->alert('error', $title . '不存在'); - } - - $typeList = $deploy == 1 ? DeployHelper::getList() : CertHelper::getList(); - $classList = $deploy == 1 ? DeployHelper::$class_config : CertHelper::$class_config; - - View::assign('title', $title); - View::assign('info', $account); - View::assign('typeList', $typeList); - View::assign('classList', $classList); - View::assign('action', $action); - View::assign('deploy', $deploy); - return View::fetch(); - } - - private function checkAccount($id, $type, $deploy) - { - if ($deploy == 0) { - $mod = CertHelper::getModel($id); - if ($mod) { - try { - $ext = $mod->register(); - if (is_array($ext)) { - Db::name('cert_account')->where('id', $id)->update(['ext' => json_encode($ext)]); - } - return true; - } catch (Exception $e) { - throw new Exception('验证SSL证书账户失败,' . $e->getMessage()); - } - } else { - throw new Exception('SSL证书申请模块' . $type . '不存在'); - } - } else { - $mod = DeployHelper::getModel($id); - if ($mod) { - try { - $mod->check(); - return true; - } catch (Exception $e) { - throw new Exception('验证自动部署账户失败,' . $e->getMessage()); - } - } else { - throw new Exception('SSL证书申请模块' . $type . '不存在'); - } - } - } - - public function certorder() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $types = []; - foreach (CertHelper::$cert_config as $key => $value) { - $types[$key] = $value['name']; - } - View::assign('types', $types); - return view(); - } - - public function order_data() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $domain = $this->request->post('domain', null, 'trim'); - $id = input('post.id'); - $aid = input('post.aid', null, 'trim'); - $type = input('post.type', null, 'trim'); - $status = input('post.status', null, 'trim'); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - - $select = Db::name('cert_order')->alias('A')->leftJoin('cert_account B', 'A.aid = B.id'); - if (!empty($id)) { - $select->where('A.id', $id); - } elseif (!empty($domain)) { - $oids = Db::name('cert_domain')->where('domain', 'like', '%' . $domain . '%')->column('oid'); - $select->whereIn('A.id', $oids); - } - if (!empty($aid)) { - $select->where('A.aid', $aid); - } - if (!empty($type)) { - $select->where('B.type', $type); - } - if (!isNullOrEmpty($status)) { - if ($status == '5') { - $select->where('A.status', '<', 0); - } elseif ($status == '6') { - $select->where('A.expiretime', '<', date('Y-m-d H:i:s', time() + 86400 * 7))->where('A.expiretime', '>=', date('Y-m-d H:i:s')); - } elseif ($status == '7') { - $select->where('A.expiretime', '<', date('Y-m-d H:i:s')); - } else { - $select->where('A.status', $status); - } - } - $total = $select->count(); - $rows = $select->fieldRaw('A.*,B.type,B.remark aremark')->order('id', 'desc')->limit($offset, $limit)->select(); - - $list = []; - foreach ($rows as $row) { - if (!empty($row['type']) && isset(CertHelper::$cert_config[$row['type']])) { - $row['typename'] = CertHelper::$cert_config[$row['type']]['name']; - $row['icon'] = CertHelper::$cert_config[$row['type']]['icon']; - } else { - $row['typename'] = null; - } - $row['domains'] = Db::name('cert_domain')->where('oid', $row['id'])->order('sort', 'ASC')->column('domain'); - $row['end_day'] = $row['expiretime'] ? ceil((strtotime($row['expiretime']) - time()) / 86400) : null; - if ($row['error']) $row['error'] = htmlspecialchars(str_replace("'", "\\'", $row['error'])); - $list[] = $row; - } - - return json(['total' => $total, 'rows' => $list]); - } - - public function order_info() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $id = input('post.id/d'); - $row = Db::name('cert_order')->where('id', $id)->find(); - if (!$row) return json(['code' => -1, 'msg' => '证书订单不存在']); - $pfx = CertHelper::getPfx($row['fullchain'], $row['privatekey']); - $row['pfx'] = base64_encode($pfx); - return json(['code' => 0, 'data' => ['id' => $row['id'], 'crt' => $row['fullchain'], 'key' => $row['privatekey'], 'pfx' => $row['pfx'], 'issuetime' => $row['issuetime'], 'expiretime' => $row['expiretime'], 'domains' => Db::name('cert_domain')->where('oid', $row['id'])->order('sort', 'ASC')->column('domain')]]); - } - - public function order_op() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - - if ($action == 'get') { - $id = input('post.id/d'); - $row = Db::name('cert_order')->where('id', $id)->field('fullchain,privatekey')->find(); - if (!$row) return $this->alert('error', '证书订单不存在'); - $pfx = CertHelper::getPfx($row['fullchain'], $row['privatekey']); - $row['pfx'] = base64_encode($pfx); - return json(['code' => 0, 'data' => $row]); - } elseif ($action == 'add') { - $aid = input('post.aid/d'); - - if ($aid == -1) { - $fullchain = input('post.fullchain', null, 'trim'); - $privatekey = input('post.privatekey', null, 'trim'); - $certInfo = $this->parse_cert_key($fullchain, $privatekey); - if ($certInfo['code'] == -1) return json($certInfo); - $domains = $certInfo['domains']; - - $order_ids = Db::name('cert_order')->where('issuetime', $certInfo['issuetime'])->column('id'); - if (!empty($order_ids)) { - foreach ($order_ids as $order_id) { - $domains2 = Db::name('cert_domain')->where('oid', $order_id)->column('domain'); - if (arrays_are_equal($domains2, $domains)) { - return json(['code' => -1, 'msg' => '该证书已存在,无需重复添加']); - } - } - } - - $order = [ - 'aid' => 0, - 'keytype' => $certInfo['keytype'], - 'keysize' => $certInfo['keysize'], - 'addtime' => date('Y-m-d H:i:s'), - 'updatetime' => date('Y-m-d H:i:s'), - 'issuetime' => $certInfo['issuetime'], - 'expiretime' => $certInfo['expiretime'], - 'issuer' => $certInfo['issuer'], - 'status' => 3, - 'isauto' => 1, - 'fullchain' => $fullchain, - 'privatekey' => $privatekey, - ]; - } else { - $order = [ - 'aid' => $aid, - 'keytype' => input('post.keytype'), - 'keysize' => input('post.keysize'), - 'addtime' => date('Y-m-d H:i:s'), - 'issuer' => '', - 'status' => 0, - 'isauto' => 1, - ]; - $domains = input('post.domains', [], 'trim'); - $domains = array_map('trim', $domains); - $domains = array_filter($domains, function ($v) { - return !empty($v); - }); - $domains = array_unique($domains); - if (empty($domains)) return json(['code' => -1, 'msg' => '绑定域名不能为空']); - $res = $this->check_order($order, $domains); - if (is_array($res)) return json($res); - } - if (empty($order['keytype']) || empty($order['keysize'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); - - Db::startTrans(); - $id = Db::name('cert_order')->insertGetId($order); - $domainList = []; - $i = 1; - foreach ($domains as $domain) { - $domainList[] = [ - 'oid' => $id, - 'domain' => convertDomainToAscii($domain), - 'sort' => $i++, - ]; - } - Db::name('cert_domain')->insertAll($domainList); - Db::commit(); - return json(['code' => 0, 'msg' => '添加证书订单成功!']); - } elseif ($action == 'edit') { - $id = input('post.id/d'); - $row = Db::name('cert_order')->where('id', $id)->find(); - if (!$row) return json(['code' => -1, 'msg' => '证书订单不存在']); - - $aid = input('post.aid/d'); - if ($aid == -1) { - $fullchain = input('post.fullchain', null, 'trim'); - $privatekey = input('post.privatekey', null, 'trim'); - $certInfo = $this->parse_cert_key($fullchain, $privatekey); - if ($certInfo['code'] == -1) return json($certInfo); - $domains = $certInfo['domains']; - - $order = [ - 'aid' => 0, - 'keytype' => $certInfo['keytype'], - 'keysize' => $certInfo['keysize'], - 'updatetime' => date('Y-m-d H:i:s'), - 'issuetime' => $certInfo['issuetime'], - 'expiretime' => $certInfo['expiretime'], - 'issuer' => $certInfo['issuer'], - 'status' => 3, - 'issend' => 0, - 'fullchain' => $fullchain, - 'privatekey' => $privatekey, - ]; - } else { - $domains = input('post.domains', [], 'trim'); - $order = [ - 'aid' => $aid, - 'keytype' => input('post.keytype'), - 'keysize' => input('post.keysize'), - 'updatetime' => date('Y-m-d H:i:s'), - ]; - $domains = array_map('trim', $domains); - $domains = array_filter($domains, function ($v) { - return !empty($v); - }); - $domains = array_unique($domains); - if (empty($domains)) return json(['code' => -1, 'msg' => '绑定域名不能为空']); - $res = $this->check_order($order, $domains); - if (is_array($res)) return json($res); - } - if (empty($order['keytype']) || empty($order['keysize'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); - - Db::startTrans(); - Db::name('cert_order')->where('id', $id)->update($order); - Db::name('cert_domain')->where('oid', $id)->delete(); - $domainList = []; - $i = 1; - foreach ($domains as $domain) { - $domainList[] = [ - 'oid' => $id, - 'domain' => convertDomainToAscii($domain), - 'sort' => $i++, - ]; - } - Db::name('cert_domain')->insertAll($domainList); - Db::commit(); - return json(['code' => 0, 'msg' => '修改证书订单成功!']); - } elseif ($action == 'del') { - $id = input('post.id/d'); - $dcount = DB::name('cert_deploy')->where('oid', $id)->count(); - if ($dcount > 0) return json(['code' => -1, 'msg' => '该证书关联了自动部署任务,无法删除']); - try { - (new CertOrderService($id))->cancel(); - } catch (Exception $e) { - } - Db::name('cert_order')->where('id', $id)->delete(); - Db::name('cert_domain')->where('oid', $id)->delete(); - return json(['code' => 0]); - } elseif ($action == 'setauto') { - $id = input('post.id/d'); - $isauto = input('post.isauto/d'); - Db::name('cert_order')->where('id', $id)->update(['isauto' => $isauto]); - return json(['code' => 0]); - } elseif ($action == 'reset') { - $id = input('post.id/d'); - try { - $service = new CertOrderService($id); - $service->cancel(); - $service->reset(); - return json(['code' => 0]); - } catch (Exception $e) { - return json(['code' => -1, 'msg' => $e->getMessage()]); - } - } elseif ($action == 'revoke') { - $id = input('post.id/d'); - try { - $service = new CertOrderService($id); - $service->revoke(); - return json(['code' => 0]); - } catch (Exception $e) { - return json(['code' => -1, 'msg' => $e->getMessage()]); - } - } elseif ($action == 'show_log') { - $processid = input('post.processid'); - $file = app()->getRuntimePath() . 'log/' . $processid . '.log'; - if (!file_exists($file)) return json(['code' => -1, 'msg' => '日志文件不存在']); - return json(['code' => 0, 'data' => file_get_contents($file), 'time' => filemtime($file)]); - } elseif ($action == 'operation') { - $ids = input('post.ids'); - $success = 0; - foreach ($ids as $id) { - if (input('post.act') == 'delete') { - $dcount = DB::name('cert_deploy')->where('oid', $id)->count(); - if ($dcount > 0) continue; - try { - (new CertOrderService($id))->cancel(); - } catch (Exception $e) { - } - Db::name('cert_order')->where('id', $id)->delete(); - Db::name('cert_domain')->where('oid', $id)->delete(); - $success++; - } elseif (input('post.act') == 'reset') { - try { - $service = new CertOrderService($id); - $service->cancel(); - $service->reset(); - $success++; - } catch (Exception $e) { - } - } elseif (input('post.act') == 'open' || input('post.act') == 'close') { - $isauto = input('post.act') == 'open' ? 1 : 0; - Db::name('cert_order')->where('id', $id)->update(['isauto' => $isauto]); - $success++; - } - } - return json(['code' => 0, 'msg' => '成功操作' . $success . '个证书订单']); - } - return json(['code' => -3]); - } - - private function check_order($order, $domains) - { - $account = Db::name('cert_account')->where('id', $order['aid'])->find(); - if (!$account) return ['code' => -1, 'msg' => 'SSL证书账户不存在']; - $max_domains = CertHelper::$cert_config[$account['type']]['max_domains']; - $wildcard = CertHelper::$cert_config[$account['type']]['wildcard']; - $cname = CertHelper::$cert_config[$account['type']]['cname']; - if (count($domains) > $max_domains) { - if (!(count($domains) == 2 && $max_domains == 1 && ltrim($domains[0], 'www.') == ltrim($domains[1], 'www.'))) { - return ['code' => -1, 'msg' => '域名数量不能超过' . $max_domains . '个']; - } - } - - foreach ($domains as $domain) { - if (!$wildcard && strpos($domain, '*') !== false) return ['code' => -1, 'msg' => '该证书账户类型不支持泛域名']; - $mainDomain = getMainDomain($domain); - $drow = Db::name('domain')->where('name', $mainDomain)->find(); - if (!$drow) { - if (substr($domain, 0, 2) == '*.') $domain = substr($domain, 2); - if (!$cname || !Db::name('cert_cname')->where('domain', $domain)->where('status', 1)->find()) { - return ['code' => -1, 'msg' => '域名' . $domain . '未在本系统添加']; - } - } - } - return true; - } - - private function parse_cert_key($fullchain, $privatekey) - { - if (!openssl_x509_read($fullchain)) return ['code' => -1, 'msg' => '证书内容填写错误']; - if (!openssl_get_privatekey($privatekey)) return ['code' => -1, 'msg' => '私钥内容填写错误']; - if (!openssl_x509_check_private_key($fullchain, $privatekey)) return ['code' => -1, 'msg' => 'SSL证书与私钥不匹配']; - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo || !isset($certInfo['extensions']['subjectAltName'])) return ['code' => -1, 'msg' => '证书内容解析失败']; - - $pubKey = openssl_pkey_get_public($fullchain); - if (!$pubKey) return ['code' => -1, 'msg' => '证书公钥解析失败']; - $keyDetails = openssl_pkey_get_details($pubKey); - $keytype = null; - $keysize = 0; - switch ($keyDetails['type']) { - case OPENSSL_KEYTYPE_RSA: - $keytype = 'RSA'; - $keysize = $keyDetails['bits']; - break; - case OPENSSL_KEYTYPE_EC: - $keytype = 'ECC'; - $keysize = $keyDetails['bits']; - break; - case OPENSSL_KEYTYPE_DSA: - $keytype = 'DSA'; - $keysize = $keyDetails['bits']; - break; - default: - $keytype = 'Unknown'; - } - - $domains = []; - $subjectAltName = explode(',', $certInfo['extensions']['subjectAltName']); - foreach ($subjectAltName as $domain) { - $domain = trim($domain); - if (strpos($domain, 'DNS:') === 0) $domain = substr($domain, 4); - if (!empty($domain)) { - $domains[] = $domain; - } - } - $domains = array_unique($domains); - if (empty($domains)) return ['code' => -1, 'msg' => '证书绑定域名不能为空']; - $issuetime = date('Y-m-d H:i:s', $certInfo['validFrom_time_t']); - $expiretime = date('Y-m-d H:i:s', $certInfo['validTo_time_t']); - $issuer = $certInfo['issuer']['CN']; - return [ - 'code' => 0, - 'keytype' => $keytype, - 'keysize' => $keysize, - 'issuetime' => $issuetime, - 'expiretime' => $expiretime, - 'issuer' => $issuer, - 'domains' => $domains, - ]; - } - - public function order_process() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - if (function_exists("set_time_limit")) { - @set_time_limit(0); - } - if (function_exists("ignore_user_abort")) { - @ignore_user_abort(true); - } - $id = input('post.id/d'); - $reset = input('post.reset/d', 0); - try { - $service = new CertOrderService($id); - if ($reset == 1) { - $service->reset(); - } - $retcode = $service->process(true); - if ($retcode == 3) { - return json(['code' => 0, 'msg' => '证书已签发成功!']); - } elseif ($retcode == 1) { - return json(['code' => 0, 'msg' => '添加DNS记录成功!请等待DNS生效后点击验证']); - } - } catch (Exception $e) { - return json(['code' => -1, 'msg' => $e->getMessage(), 'trace' => $e->getTrace()]); - } - } - - public function order_form() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - - $order = null; - if ($action == 'edit') { - $id = input('get.id/d'); - $order = Db::name('cert_order')->where('id', $id)->fieldRaw('id,aid,keytype,keysize,status,fullchain,privatekey')->find(); - if (empty($order)) return $this->alert('error', '证书订单不存在'); - $order['domains'] = Db::name('cert_domain')->where('oid', $order['id'])->order('sort', 'ASC')->column('domain'); - if ($order['aid'] == 0) $order['aid'] = -1; - } - - $accounts = []; - foreach (Db::name('cert_account')->where('deploy', 0)->select() as $row) { - if (empty($row['type']) || !isset(CertHelper::$cert_config[$row['type']])) continue; - $accounts[$row['id']] = ['name' => $row['id'] . '_' . CertHelper::$cert_config[$row['type']]['name'], 'type' => $row['type']]; - if (!empty($row['remark'])) { - $accounts[$row['id']]['name'] .= '(' . $row['remark'] . ')'; - } - } - View::assign('accounts', $accounts); - - View::assign('info', $order); - View::assign('action', $action); - return View::fetch(); - } - - public function deploytask() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $types = []; - foreach (DeployHelper::$deploy_config as $key => $value) { - $types[$key] = $value['name']; - } - View::assign('types', $types); - return view(); - } - - public function deploy_data() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $domain = $this->request->post('domain', null, 'trim'); - $oid = input('post.oid'); - $aid = input('post.aid', null, 'trim'); - $type = input('post.type', null, 'trim'); - $status = input('post.status', null, 'trim'); - $remark = input('post.remark', null, 'trim'); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - - $select = Db::name('cert_deploy')->alias('A')->leftJoin('cert_account B', 'A.aid = B.id')->leftJoin('cert_order C', 'A.oid = C.id')->leftJoin('cert_account D', 'C.aid = D.id'); - if (!empty($oid)) { - $select->where('A.oid', $oid); - } elseif (!empty($domain)) { - $oids = Db::name('cert_domain')->where('domain', 'like', '%' . $domain . '%')->column('oid'); - $select->whereIn('oid', $oids); - } - if (!empty($aid)) { - $select->where('A.aid', $aid); - } - if (!empty($type)) { - $select->where('B.type', $type); - } - if (!isNullOrEmpty($status)) { - $select->where('A.status', $status); - } - if (!empty($remark)) { - $select->where('A.remark', $remark); - } - $total = $select->count(); - $rows = $select->fieldRaw('A.*,B.type,B.remark aremark,B.name aname,D.type certtype,D.id certaid')->order('id', 'desc')->limit($offset, $limit)->select(); - - $list = []; - foreach ($rows as $row) { - if (!empty($row['type']) && isset(DeployHelper::$deploy_config[$row['type']])) { - $row['typename'] = DeployHelper::$deploy_config[$row['type']]['name']; - $row['icon'] = DeployHelper::$deploy_config[$row['type']]['icon']; - } - if (!empty($row['certtype']) && isset(CertHelper::$cert_config[$row['certtype']])) { - $row['certtypename'] = CertHelper::$cert_config[$row['certtype']]['name']; - } else { - $row['certtypename'] = '手动续期'; - } - $row['domains'] = Db::name('cert_domain')->where('oid', $row['oid'])->order('sort', 'ASC')->column('domain'); - if ($row['error']) $row['error'] = htmlspecialchars(str_replace("'", "\\'", $row['error'])); - $list[] = $row; - } - - return json(['total' => $total, 'rows' => $list]); - } - - public function deploy_op() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - - if ($action == 'add') { - $task = [ - 'aid' => input('post.aid/d'), - 'oid' => input('post.oid/d'), - 'config' => input('post.config', null, 'trim'), - 'remark' => input('post.remark', null, 'trim'), - 'addtime' => date('Y-m-d H:i:s'), - 'status' => 0, - 'active' => 1 - ]; - if (empty($task['aid']) || empty($task['oid']) || empty($task['config'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); - Db::name('cert_deploy')->insert($task); - return json(['code' => 0, 'msg' => '添加自动部署任务成功!']); - } elseif ($action == 'edit') { - $id = input('post.id/d'); - $row = Db::name('cert_deploy')->where('id', $id)->find(); - if (!$row) return json(['code' => -1, 'msg' => '自动部署任务不存在']); - - $task = [ - 'aid' => input('post.aid/d'), - 'oid' => input('post.oid/d'), - 'config' => input('post.config', null, 'trim'), - 'remark' => input('post.remark', null, 'trim'), - ]; - if (empty($task['aid']) || empty($task['oid']) || empty($task['config'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); - Db::name('cert_deploy')->where('id', $id)->update($task); - return json(['code' => 0, 'msg' => '修改自动部署任务成功!']); - } elseif ($action == 'del') { - $id = input('post.id/d'); - Db::name('cert_deploy')->where('id', $id)->delete(); - return json(['code' => 0]); - } elseif ($action == 'setactive') { - $id = input('post.id/d'); - $active = input('post.active/d'); - Db::name('cert_deploy')->where('id', $id)->update(['active' => $active]); - return json(['code' => 0]); - } elseif ($action == 'reset') { - $id = input('post.id/d'); - try { - $service = new CertDeployService($id); - $service->reset(); - return json(['code' => 0]); - } catch (Exception $e) { - return json(['code' => -1, 'msg' => $e->getMessage()]); - } - } elseif ($action == 'show_log') { - $processid = input('post.processid'); - $file = app()->getRuntimePath() . 'log/' . $processid . '.log'; - if (!file_exists($file)) return json(['code' => -1, 'msg' => '日志文件不存在']); - return json(['code' => 0, 'data' => file_get_contents($file), 'time' => filemtime($file)]); - } elseif ($action == 'operation') { - $ids = input('post.ids'); - $success = 0; - $certid = 0; - if (input('post.action') == 'cert') { - $certid = input('post.certid/d'); - $cert = Db::name('cert_order')->where('id', $certid)->find(); - if (!$cert) return json(['code' => -1, 'msg' => '证书订单不存在']); - } - foreach ($ids as $id) { - if (input('post.act') == 'delete') { - Db::name('cert_deploy')->where('id', $id)->delete(); - $success++; - } elseif (input('post.act') == 'reset') { - try { - $service = new CertDeployService($id); - $service->reset(); - $success++; - } catch (Exception $e) { - } - } elseif (input('post.act') == 'open' || input('post.act') == 'close') { - $active = input('post.act') == 'open' ? 1 : 0; - Db::name('cert_deploy')->where('id', $id)->update(['active' => $active]); - $success++; - } elseif (input('post.act') == 'cert') { - Db::name('cert_deploy')->where('id', $id)->update(['oid' => $certid]); - $success++; - } - } - return json(['code' => 0, 'msg' => '成功操作' . $success . '个任务']); - } - return json(['code' => -3]); - } - - public function deploy_process() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - if (function_exists("set_time_limit")) { - @set_time_limit(0); - } - if (function_exists("ignore_user_abort")) { - @ignore_user_abort(true); - } - $id = input('post.id/d'); - $reset = input('post.reset/d', 0); - try { - $service = new CertDeployService($id); - if ($reset == 1) { - $service->reset(); - } - $service->process(true); - return json(['code' => 0, 'msg' => 'SSL证书部署任务执行成功!']); - } catch (Exception $e) { - return json(['code' => -1, 'msg' => $e->getMessage(), 'trace' => $e->getTrace()]); - } - } - - public function deploy_form() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - - $task = null; - if ($action == 'edit') { - $id = input('get.id/d'); - $task = Db::name('cert_deploy')->alias('A')->join('cert_account B', 'A.aid = B.id')->where('A.id', $id)->fieldRaw('A.id,A.aid,A.oid,A.config,A.remark,B.type')->find(); - if (empty($task)) return $this->alert('error', '自动部署任务不存在'); - } - - $accounts = []; - foreach (Db::name('cert_account')->where('deploy', 1)->select() as $row) { - if (empty($row['type']) || !isset(DeployHelper::$deploy_config[$row['type']])) continue; - $accounts[$row['id']] = ['name' => $row['id'] . '_' . DeployHelper::$deploy_config[$row['type']]['name'], 'type' => $row['type']]; - if (!empty($row['remark'])) { - $accounts[$row['id']]['name'] .= '(' . $row['remark'] . ')'; - } - } - View::assign('accounts', $accounts); - - $orders = []; - foreach (Db::name('cert_order')->alias('A')->leftJoin('cert_account B', 'A.aid = B.id')->where('status', '<>', 4)->fieldRaw('A.id,A.aid,B.type,B.remark aremark')->order('id', 'desc')->select() as $row) { - $domains = Db::name('cert_domain')->where('oid', $row['id'])->order('sort', 'ASC')->column('domain'); - $domainstr = count($domains) > 2 ? implode('、', array_slice($domains, 0, 2)) . '等' . count($domains) . '个域名' : implode('、', $domains); - if ($row['aid'] == 0) { - $name = $row['id'] . '_' . $domainstr . '(手动续期)'; - } else { - $name = $row['id'] . '_' . $domainstr . '(' . CertHelper::$cert_config[$row['type']]['name'] . ')'; - } - $orders[$row['id']] = ['name' => $name]; - } - View::assign('orders', $orders); - - View::assign('info', $task); - View::assign('action', $action); - View::assign('typeList', DeployHelper::getList()); - return View::fetch(); - } - - public function cname() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $domains = []; - foreach (Db::name('domain')->field('id,name')->select() as $row) { - $domains[$row['id']] = $row['name']; - } - View::assign('domains', $domains); - return view(); - } - - public function cname_data() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $kw = $this->request->post('kw', null, 'trim'); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - - $select = Db::name('cert_cname')->alias('A')->leftJoin('domain B', 'A.did = B.id'); - if (!empty($kw)) { - $select->whereLike('A.domain', '%' . $kw . '%'); - } - $total = $select->count(); - $rows = $select->order('A.id', 'desc')->limit($offset, $limit)->field('A.*,B.name cnamedomain')->select(); - - $list = []; - foreach ($rows as $row) { - $row['host'] = $this->getCnameHost($row['domain']); - $row['record'] = $row['rr'] . '.' . $row['cnamedomain']; - $list[] = $row; - } - - return json(['total' => $total, 'rows' => $list]); - } - - private function getCnameHost($domain) - { - $main = getMainDomain($domain); - if ($main == $domain) { - return '_acme-challenge'; - } else { - return '_acme-challenge.' . substr($domain, 0, -strlen($main) - 1); - } - } - - public function cname_op() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - - if ($action == 'add') { - $data = [ - 'domain' => input('post.domain', null, 'trim'), - 'rr' => input('post.rr', null, 'trim'), - 'did' => input('post.did/d'), - 'addtime' => date('Y-m-d H:i:s'), - 'status' => 0 - ]; - if (empty($data['domain']) || empty($data['rr']) || empty($data['did'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); - if (!checkDomain($data['domain'])) return json(['code' => -1, 'msg' => '域名格式不正确']); - if (Db::name('cert_cname')->where('domain', $data['domain'])->find()) { - return json(['code' => -1, 'msg' => '域名' . $data['domain'] . '已存在']); - } - if (Db::name('cert_cname')->where('rr', $data['rr'])->where('did', $data['did'])->find()) { - return json(['code' => -1, 'msg' => '已存在相同CNAME记录值']); - } - Db::name('cert_cname')->insert($data); - return json(['code' => 0, 'msg' => '添加CMAME代理成功!']); - } elseif ($action == 'edit') { - $id = input('post.id/d'); - $row = Db::name('cert_cname')->where('id', $id)->find(); - if (!$row) return json(['code' => -1, 'msg' => 'CMAME代理不存在']); - - $data = [ - 'rr' => input('post.rr', null, 'trim'), - 'did' => input('post.did/d'), - ]; - if ($row['rr'] != $data['rr'] || $row['did'] != $data['did']) { - $data['status'] = 0; - } - if (empty($data['rr']) || empty($data['did'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); - if (Db::name('cert_cname')->where('rr', $data['rr'])->where('did', $data['did'])->where('id', '<>', $id)->find()) { - return json(['code' => -1, 'msg' => '已存在相同CNAME记录值']); - } - Db::name('cert_cname')->where('id', $id)->update($data); - return json(['code' => 0, 'msg' => '修改CMAME代理成功!']); - } elseif ($action == 'del') { - $id = input('post.id/d'); - Db::name('cert_cname')->where('id', $id)->delete(); - return json(['code' => 0]); - } elseif ($action == 'check') { - $id = input('post.id/d'); - $row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.id', $id)->field('A.*,B.name cnamedomain')->find(); - if (!$row) return json(['code' => -1, 'msg' => '自动部署任务不存在']); - - $status = 1; - $domain = '_acme-challenge.' . $row['domain']; - $record = $row['rr'] . '.' . $row['cnamedomain']; - $result = \app\utils\DnsQueryUtils::get_dns_records($domain, 'CNAME'); - if (!$result || !in_array($record, $result)) { - $result = \app\utils\DnsQueryUtils::query_dns_doh($domain, 'CNAME'); - if (!$result || !in_array($record, $result)) { - $status = 0; - } - } - if ($status != $row['status']) { - Db::name('cert_cname')->where('id', $id)->update(['status' => $status]); - } - return json(['code' => 0, 'status' => $status]); - } - } - - public function certset() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - return View::fetch(); - } -} +alert('error', '无权限'); + return view(); + } + + public function deployaccount() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + return view(); + } + + public function account_data() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $deploy = input('get.deploy/d', 0); + $kw = $this->request->post('kw', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('cert_account')->where('deploy', $deploy); + if (!empty($kw)) { + $select->whereLike('name|remark', '%' . $kw . '%')->whereOr('id', $kw); + } + $total = $select->count(); + $rows = $select->order('id', 'desc')->limit($offset, $limit)->select(); + + $list = []; + foreach ($rows as $row) { + if ($deploy == 1) { + if (!empty($row['type']) && isset(DeployHelper::$deploy_config[$row['type']])) { + $row['typename'] = DeployHelper::$deploy_config[$row['type']]['name']; + $row['icon'] = DeployHelper::$deploy_config[$row['type']]['icon']; + } + } else { + if (!empty($row['type']) && isset(CertHelper::$cert_config[$row['type']])) { + $row['typename'] = CertHelper::$cert_config[$row['type']]['name']; + $row['icon'] = CertHelper::$cert_config[$row['type']]['icon']; + } + } + $list[] = $row; + } + + return json(['total' => $total, 'rows' => $list]); + } + + public function account_op() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + $deploy = input('post.deploy/d', 0); + $title = $deploy == 1 ? '自动部署账户' : 'SSL证书账户'; + + if ($action == 'add') { + $type = input('post.type'); + $name = input('post.name', null, 'trim'); + $config = input('post.config', null, 'trim'); + $remark = input('post.remark', null, 'trim'); + if ($type == 'local') $name = '复制到本机'; + if (empty($name) || empty($config)) return json(['code' => -1, 'msg' => '必填参数不能为空']); + if (Db::name('cert_account')->where('type', $type)->where('config', $config)->find()) { + return json(['code' => -1, 'msg' => $title . '已存在']); + } + Db::startTrans(); + $id = Db::name('cert_account')->insertGetId([ + 'type' => $type, + 'name' => $name, + 'config' => $config, + 'remark' => $remark, + 'deploy' => $deploy, + 'addtime' => date('Y-m-d H:i:s'), + ]); + try { + $this->checkAccount($id, $type, $deploy); + Db::commit(); + return json(['code' => 0, 'msg' => '添加' . $title . '成功!']); + } catch (Exception $e) { + Db::rollback(); + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } elseif ($action == 'edit') { + $id = input('post.id/d'); + $row = Db::name('cert_account')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => $title . '不存在']); + $type = input('post.type'); + $name = input('post.name', null, 'trim'); + $config = input('post.config', null, 'trim'); + $remark = input('post.remark', null, 'trim'); + if ($type == 'local') $name = '复制到本机'; + if (empty($name) || empty($config)) return json(['code' => -1, 'msg' => '必填参数不能为空']); + if (Db::name('cert_account')->where('type', $type)->where('config', $config)->where('id', '<>', $id)->find()) { + return json(['code' => -1, 'msg' => $title . '已存在']); + } + Db::startTrans(); + Db::name('cert_account')->where('id', $id)->update([ + 'type' => $type, + 'name' => $name, + 'config' => $config, + 'remark' => $remark, + ]); + try { + $this->checkAccount($id, $type, $deploy); + Db::commit(); + return json(['code' => 0, 'msg' => '修改' . $title . '成功!']); + } catch (Exception $e) { + Db::rollback(); + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } elseif ($action == 'del') { + $id = input('post.id/d'); + if ($deploy == 0) { + $dcount = DB::name('cert_order')->where('aid', $id)->count(); + if ($dcount > 0) return json(['code' => -1, 'msg' => '该' . $title . '下存在证书订单,无法删除']); + } else { + $dcount = DB::name('cert_deploy')->where('aid', $id)->count(); + if ($dcount > 0) return json(['code' => -1, 'msg' => '该' . $title . '下存在自动部署任务,无法删除']); + } + Db::name('cert_account')->where('id', $id)->delete(); + return json(['code' => 0]); + } + return json(['code' => -3]); + } + + public function account_form() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + $deploy = input('get.deploy/d', 0); + $title = $deploy == 1 ? '自动部署账户' : 'SSL证书账户'; + + $account = null; + if ($action == 'edit') { + $id = input('get.id/d'); + $account = Db::name('cert_account')->where('id', $id)->find(); + if (empty($account)) return $this->alert('error', $title . '不存在'); + } + + $typeList = $deploy == 1 ? DeployHelper::getList() : CertHelper::getList(); + $classList = $deploy == 1 ? DeployHelper::$class_config : CertHelper::$class_config; + + View::assign('title', $title); + View::assign('info', $account); + View::assign('typeList', $typeList); + View::assign('classList', $classList); + View::assign('action', $action); + View::assign('deploy', $deploy); + return View::fetch(); + } + + private function checkAccount($id, $type, $deploy) + { + if ($deploy == 0) { + $mod = CertHelper::getModel($id); + if ($mod) { + try { + $ext = $mod->register(); + if (is_array($ext)) { + Db::name('cert_account')->where('id', $id)->update(['ext' => json_encode($ext)]); + } + return true; + } catch (Exception $e) { + throw new Exception('验证SSL证书账户失败,' . $e->getMessage()); + } + } else { + throw new Exception('SSL证书申请模块' . $type . '不存在'); + } + } else { + $mod = DeployHelper::getModel($id); + if ($mod) { + try { + $mod->check(); + return true; + } catch (Exception $e) { + throw new Exception('验证自动部署账户失败,' . $e->getMessage()); + } + } else { + throw new Exception('SSL证书申请模块' . $type . '不存在'); + } + } + } + + public function certorder() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $types = []; + foreach (CertHelper::$cert_config as $key => $value) { + $types[$key] = $value['name']; + } + View::assign('types', $types); + return view(); + } + + public function order_data() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $domain = $this->request->post('domain', null, 'trim'); + $id = input('post.id'); + $aid = input('post.aid', null, 'trim'); + $type = input('post.type', null, 'trim'); + $status = input('post.status', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('cert_order')->alias('A')->leftJoin('cert_account B', 'A.aid = B.id'); + if (!empty($id)) { + $select->where('A.id', $id); + } elseif (!empty($domain)) { + $oids = Db::name('cert_domain')->where('domain', 'like', '%' . $domain . '%')->column('oid'); + $select->whereIn('A.id', $oids); + } + if (!empty($aid)) { + $select->where('A.aid', $aid); + } + if (!empty($type)) { + $select->where('B.type', $type); + } + if (!isNullOrEmpty($status)) { + if ($status == '5') { + $select->where('A.status', '<', 0); + } elseif ($status == '6') { + $select->where('A.expiretime', '<', date('Y-m-d H:i:s', time() + 86400 * 7))->where('A.expiretime', '>=', date('Y-m-d H:i:s')); + } elseif ($status == '7') { + $select->where('A.expiretime', '<', date('Y-m-d H:i:s')); + } else { + $select->where('A.status', $status); + } + } + $total = $select->count(); + $rows = $select->fieldRaw('A.*,B.type,B.remark aremark')->order('id', 'desc')->limit($offset, $limit)->select(); + + $list = []; + foreach ($rows as $row) { + if (!empty($row['type']) && isset(CertHelper::$cert_config[$row['type']])) { + $row['typename'] = CertHelper::$cert_config[$row['type']]['name']; + $row['icon'] = CertHelper::$cert_config[$row['type']]['icon']; + } else { + $row['typename'] = null; + } + $row['domains'] = Db::name('cert_domain')->where('oid', $row['id'])->order('sort', 'ASC')->column('domain'); + $row['end_day'] = $row['expiretime'] ? ceil((strtotime($row['expiretime']) - time()) / 86400) : null; + if ($row['error']) $row['error'] = htmlspecialchars(str_replace("'", "\\'", $row['error'])); + $list[] = $row; + } + + return json(['total' => $total, 'rows' => $list]); + } + + public function order_info() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $id = input('post.id/d'); + $row = Db::name('cert_order')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => '证书订单不存在']); + $pfx = CertHelper::getPfx($row['fullchain'], $row['privatekey']); + $row['pfx'] = base64_encode($pfx); + return json(['code' => 0, 'data' => ['id' => $row['id'], 'crt' => $row['fullchain'], 'key' => $row['privatekey'], 'pfx' => $row['pfx'], 'issuetime' => $row['issuetime'], 'expiretime' => $row['expiretime'], 'domains' => Db::name('cert_domain')->where('oid', $row['id'])->order('sort', 'ASC')->column('domain')]]); + } + + public function order_op() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + + if ($action == 'get') { + $id = input('post.id/d'); + $row = Db::name('cert_order')->where('id', $id)->field('fullchain,privatekey')->find(); + if (!$row) return $this->alert('error', '证书订单不存在'); + $pfx = CertHelper::getPfx($row['fullchain'], $row['privatekey']); + $row['pfx'] = base64_encode($pfx); + return json(['code' => 0, 'data' => $row]); + } elseif ($action == 'add') { + $aid = input('post.aid/d'); + + if ($aid == -1) { + $fullchain = input('post.fullchain', null, 'trim'); + $privatekey = input('post.privatekey', null, 'trim'); + $certInfo = $this->parse_cert_key($fullchain, $privatekey); + if ($certInfo['code'] == -1) return json($certInfo); + $domains = $certInfo['domains']; + + $order_ids = Db::name('cert_order')->where('issuetime', $certInfo['issuetime'])->column('id'); + if (!empty($order_ids)) { + foreach ($order_ids as $order_id) { + $domains2 = Db::name('cert_domain')->where('oid', $order_id)->column('domain'); + if (arrays_are_equal($domains2, $domains)) { + return json(['code' => -1, 'msg' => '该证书已存在,无需重复添加']); + } + } + } + + $order = [ + 'aid' => 0, + 'keytype' => $certInfo['keytype'], + 'keysize' => $certInfo['keysize'], + 'addtime' => date('Y-m-d H:i:s'), + 'updatetime' => date('Y-m-d H:i:s'), + 'issuetime' => $certInfo['issuetime'], + 'expiretime' => $certInfo['expiretime'], + 'issuer' => $certInfo['issuer'], + 'status' => 3, + 'isauto' => 1, + 'fullchain' => $fullchain, + 'privatekey' => $privatekey, + ]; + } else { + $order = [ + 'aid' => $aid, + 'keytype' => input('post.keytype'), + 'keysize' => input('post.keysize'), + 'addtime' => date('Y-m-d H:i:s'), + 'issuer' => '', + 'status' => 0, + 'isauto' => 1, + ]; + $domains = input('post.domains', [], 'trim'); + $domains = array_map('trim', $domains); + $domains = array_filter($domains, function ($v) { + return !empty($v); + }); + $domains = array_unique($domains); + if (empty($domains)) return json(['code' => -1, 'msg' => '绑定域名不能为空']); + $res = $this->check_order($order, $domains); + if (is_array($res)) return json($res); + } + if (empty($order['keytype']) || empty($order['keysize'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + + Db::startTrans(); + $id = Db::name('cert_order')->insertGetId($order); + $domainList = []; + $i = 1; + foreach ($domains as $domain) { + $domainList[] = [ + 'oid' => $id, + 'domain' => convertDomainToAscii($domain), + 'sort' => $i++, + ]; + } + Db::name('cert_domain')->insertAll($domainList); + Db::commit(); + return json(['code' => 0, 'msg' => '添加证书订单成功!']); + } elseif ($action == 'edit') { + $id = input('post.id/d'); + $row = Db::name('cert_order')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => '证书订单不存在']); + + $aid = input('post.aid/d'); + if ($aid == -1) { + $fullchain = input('post.fullchain', null, 'trim'); + $privatekey = input('post.privatekey', null, 'trim'); + $certInfo = $this->parse_cert_key($fullchain, $privatekey); + if ($certInfo['code'] == -1) return json($certInfo); + $domains = $certInfo['domains']; + + $order = [ + 'aid' => 0, + 'keytype' => $certInfo['keytype'], + 'keysize' => $certInfo['keysize'], + 'updatetime' => date('Y-m-d H:i:s'), + 'issuetime' => $certInfo['issuetime'], + 'expiretime' => $certInfo['expiretime'], + 'issuer' => $certInfo['issuer'], + 'status' => 3, + 'issend' => 0, + 'fullchain' => $fullchain, + 'privatekey' => $privatekey, + ]; + } else { + $domains = input('post.domains', [], 'trim'); + $order = [ + 'aid' => $aid, + 'keytype' => input('post.keytype'), + 'keysize' => input('post.keysize'), + 'updatetime' => date('Y-m-d H:i:s'), + ]; + $domains = array_map('trim', $domains); + $domains = array_filter($domains, function ($v) { + return !empty($v); + }); + $domains = array_unique($domains); + if (empty($domains)) return json(['code' => -1, 'msg' => '绑定域名不能为空']); + $res = $this->check_order($order, $domains); + if (is_array($res)) return json($res); + } + if (empty($order['keytype']) || empty($order['keysize'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + + Db::startTrans(); + Db::name('cert_order')->where('id', $id)->update($order); + Db::name('cert_domain')->where('oid', $id)->delete(); + $domainList = []; + $i = 1; + foreach ($domains as $domain) { + $domainList[] = [ + 'oid' => $id, + 'domain' => convertDomainToAscii($domain), + 'sort' => $i++, + ]; + } + Db::name('cert_domain')->insertAll($domainList); + Db::commit(); + return json(['code' => 0, 'msg' => '修改证书订单成功!']); + } elseif ($action == 'del') { + $id = input('post.id/d'); + $dcount = DB::name('cert_deploy')->where('oid', $id)->count(); + if ($dcount > 0) return json(['code' => -1, 'msg' => '该证书关联了自动部署任务,无法删除']); + try { + (new CertOrderService($id))->cancel(); + } catch (Exception $e) { + } + Db::name('cert_order')->where('id', $id)->delete(); + Db::name('cert_domain')->where('oid', $id)->delete(); + return json(['code' => 0]); + } elseif ($action == 'setauto') { + $id = input('post.id/d'); + $isauto = input('post.isauto/d'); + Db::name('cert_order')->where('id', $id)->update(['isauto' => $isauto]); + return json(['code' => 0]); + } elseif ($action == 'reset') { + $id = input('post.id/d'); + try { + $service = new CertOrderService($id); + $service->cancel(); + $service->reset(); + return json(['code' => 0]); + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } elseif ($action == 'revoke') { + $id = input('post.id/d'); + try { + $service = new CertOrderService($id); + $service->revoke(); + return json(['code' => 0]); + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } elseif ($action == 'show_log') { + $processid = input('post.processid'); + $file = app()->getRuntimePath() . 'log/' . $processid . '.log'; + if (!file_exists($file)) return json(['code' => -1, 'msg' => '日志文件不存在']); + return json(['code' => 0, 'data' => file_get_contents($file), 'time' => filemtime($file)]); + } elseif ($action == 'operation') { + $ids = input('post.ids'); + $success = 0; + foreach ($ids as $id) { + if (input('post.act') == 'delete') { + $dcount = DB::name('cert_deploy')->where('oid', $id)->count(); + if ($dcount > 0) continue; + try { + (new CertOrderService($id))->cancel(); + } catch (Exception $e) { + } + Db::name('cert_order')->where('id', $id)->delete(); + Db::name('cert_domain')->where('oid', $id)->delete(); + $success++; + } elseif (input('post.act') == 'reset') { + try { + $service = new CertOrderService($id); + $service->cancel(); + $service->reset(); + $success++; + } catch (Exception $e) { + } + } elseif (input('post.act') == 'open' || input('post.act') == 'close') { + $isauto = input('post.act') == 'open' ? 1 : 0; + Db::name('cert_order')->where('id', $id)->update(['isauto' => $isauto]); + $success++; + } + } + return json(['code' => 0, 'msg' => '成功操作' . $success . '个证书订单']); + } + return json(['code' => -3]); + } + + private function check_order($order, $domains) + { + $account = Db::name('cert_account')->where('id', $order['aid'])->find(); + if (!$account) return ['code' => -1, 'msg' => 'SSL证书账户不存在']; + $max_domains = CertHelper::$cert_config[$account['type']]['max_domains']; + $wildcard = CertHelper::$cert_config[$account['type']]['wildcard']; + $cname = CertHelper::$cert_config[$account['type']]['cname']; + if (count($domains) > $max_domains) { + if (!(count($domains) == 2 && $max_domains == 1 && ltrim($domains[0], 'www.') == ltrim($domains[1], 'www.'))) { + return ['code' => -1, 'msg' => '域名数量不能超过' . $max_domains . '个']; + } + } + + foreach ($domains as $domain) { + if (!$wildcard && strpos($domain, '*') !== false) return ['code' => -1, 'msg' => '该证书账户类型不支持泛域名']; + $mainDomain = getMainDomain($domain); + $drow = Db::name('domain')->where('name', $mainDomain)->find(); + if (!$drow) { + if (substr($domain, 0, 2) == '*.') $domain = substr($domain, 2); + if (!$cname || !Db::name('cert_cname')->where('domain', $domain)->where('status', 1)->find()) { + return ['code' => -1, 'msg' => '域名' . $domain . '未在本系统添加']; + } + } + } + return true; + } + + private function parse_cert_key($fullchain, $privatekey) + { + if (!openssl_x509_read($fullchain)) return ['code' => -1, 'msg' => '证书内容填写错误']; + if (!openssl_get_privatekey($privatekey)) return ['code' => -1, 'msg' => '私钥内容填写错误']; + if (!openssl_x509_check_private_key($fullchain, $privatekey)) return ['code' => -1, 'msg' => 'SSL证书与私钥不匹配']; + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo || !isset($certInfo['extensions']['subjectAltName'])) return ['code' => -1, 'msg' => '证书内容解析失败']; + + $pubKey = openssl_pkey_get_public($fullchain); + if (!$pubKey) return ['code' => -1, 'msg' => '证书公钥解析失败']; + $keyDetails = openssl_pkey_get_details($pubKey); + $keytype = null; + $keysize = 0; + switch ($keyDetails['type']) { + case OPENSSL_KEYTYPE_RSA: + $keytype = 'RSA'; + $keysize = $keyDetails['bits']; + break; + case OPENSSL_KEYTYPE_EC: + $keytype = 'ECC'; + $keysize = $keyDetails['bits']; + break; + case OPENSSL_KEYTYPE_DSA: + $keytype = 'DSA'; + $keysize = $keyDetails['bits']; + break; + default: + $keytype = 'Unknown'; + } + + $domains = []; + $subjectAltName = explode(',', $certInfo['extensions']['subjectAltName']); + foreach ($subjectAltName as $domain) { + $domain = trim($domain); + if (strpos($domain, 'DNS:') === 0) $domain = substr($domain, 4); + if (!empty($domain)) { + $domains[] = $domain; + } + } + $domains = array_unique($domains); + if (empty($domains)) return ['code' => -1, 'msg' => '证书绑定域名不能为空']; + $issuetime = date('Y-m-d H:i:s', $certInfo['validFrom_time_t']); + $expiretime = date('Y-m-d H:i:s', $certInfo['validTo_time_t']); + $issuer = $certInfo['issuer']['CN']; + return [ + 'code' => 0, + 'keytype' => $keytype, + 'keysize' => $keysize, + 'issuetime' => $issuetime, + 'expiretime' => $expiretime, + 'issuer' => $issuer, + 'domains' => $domains, + ]; + } + + public function order_process() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + if (function_exists("set_time_limit")) { + @set_time_limit(0); + } + if (function_exists("ignore_user_abort")) { + @ignore_user_abort(true); + } + $id = input('post.id/d'); + $reset = input('post.reset/d', 0); + try { + $service = new CertOrderService($id); + if ($reset == 1) { + $service->reset(); + } + $retcode = $service->process(true); + if ($retcode == 3) { + return json(['code' => 0, 'msg' => '证书已签发成功!']); + } elseif ($retcode == 1) { + return json(['code' => 0, 'msg' => '添加DNS记录成功!请等待DNS生效后点击验证']); + } + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage(), 'trace' => $e->getTrace()]); + } + } + + public function order_form() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + + $order = null; + if ($action == 'edit') { + $id = input('get.id/d'); + $order = Db::name('cert_order')->where('id', $id)->fieldRaw('id,aid,keytype,keysize,status,fullchain,privatekey')->find(); + if (empty($order)) return $this->alert('error', '证书订单不存在'); + $order['domains'] = Db::name('cert_domain')->where('oid', $order['id'])->order('sort', 'ASC')->column('domain'); + if ($order['aid'] == 0) $order['aid'] = -1; + } + + $accounts = []; + foreach (Db::name('cert_account')->where('deploy', 0)->select() as $row) { + if (empty($row['type']) || !isset(CertHelper::$cert_config[$row['type']])) continue; + $accounts[$row['id']] = ['name' => $row['id'] . '_' . CertHelper::$cert_config[$row['type']]['name'], 'type' => $row['type']]; + if (!empty($row['remark'])) { + $accounts[$row['id']]['name'] .= '(' . $row['remark'] . ')'; + } + } + View::assign('accounts', $accounts); + + View::assign('info', $order); + View::assign('action', $action); + return View::fetch(); + } + + public function deploytask() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $types = []; + foreach (DeployHelper::$deploy_config as $key => $value) { + $types[$key] = $value['name']; + } + View::assign('types', $types); + return view(); + } + + public function deploy_data() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $domain = $this->request->post('domain', null, 'trim'); + $oid = input('post.oid'); + $aid = input('post.aid', null, 'trim'); + $type = input('post.type', null, 'trim'); + $status = input('post.status', null, 'trim'); + $remark = input('post.remark', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('cert_deploy')->alias('A')->leftJoin('cert_account B', 'A.aid = B.id')->leftJoin('cert_order C', 'A.oid = C.id')->leftJoin('cert_account D', 'C.aid = D.id'); + if (!empty($oid)) { + $select->where('A.oid', $oid); + } elseif (!empty($domain)) { + $oids = Db::name('cert_domain')->where('domain', 'like', '%' . $domain . '%')->column('oid'); + $select->whereIn('oid', $oids); + } + if (!empty($aid)) { + $select->where('A.aid', $aid); + } + if (!empty($type)) { + $select->where('B.type', $type); + } + if (!isNullOrEmpty($status)) { + $select->where('A.status', $status); + } + if (!empty($remark)) { + $select->where('A.remark', $remark); + } + $total = $select->count(); + $rows = $select->fieldRaw('A.*,B.type,B.remark aremark,B.name aname,D.type certtype,D.id certaid')->order('id', 'desc')->limit($offset, $limit)->select(); + + $list = []; + foreach ($rows as $row) { + if (!empty($row['type']) && isset(DeployHelper::$deploy_config[$row['type']])) { + $row['typename'] = DeployHelper::$deploy_config[$row['type']]['name']; + $row['icon'] = DeployHelper::$deploy_config[$row['type']]['icon']; + } + if (!empty($row['certtype']) && isset(CertHelper::$cert_config[$row['certtype']])) { + $row['certtypename'] = CertHelper::$cert_config[$row['certtype']]['name']; + } else { + $row['certtypename'] = '手动续期'; + } + $row['domains'] = Db::name('cert_domain')->where('oid', $row['oid'])->order('sort', 'ASC')->column('domain'); + if ($row['error']) $row['error'] = htmlspecialchars(str_replace("'", "\\'", $row['error'])); + $list[] = $row; + } + + return json(['total' => $total, 'rows' => $list]); + } + + public function deploy_op() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + + if ($action == 'add') { + $task = [ + 'aid' => input('post.aid/d'), + 'oid' => input('post.oid/d'), + 'config' => input('post.config', null, 'trim'), + 'remark' => input('post.remark', null, 'trim'), + 'addtime' => date('Y-m-d H:i:s'), + 'status' => 0, + 'active' => 1 + ]; + if (empty($task['aid']) || empty($task['oid']) || empty($task['config'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + Db::name('cert_deploy')->insert($task); + return json(['code' => 0, 'msg' => '添加自动部署任务成功!']); + } elseif ($action == 'edit') { + $id = input('post.id/d'); + $row = Db::name('cert_deploy')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => '自动部署任务不存在']); + + $task = [ + 'aid' => input('post.aid/d'), + 'oid' => input('post.oid/d'), + 'config' => input('post.config', null, 'trim'), + 'remark' => input('post.remark', null, 'trim'), + ]; + if (empty($task['aid']) || empty($task['oid']) || empty($task['config'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + Db::name('cert_deploy')->where('id', $id)->update($task); + return json(['code' => 0, 'msg' => '修改自动部署任务成功!']); + } elseif ($action == 'del') { + $id = input('post.id/d'); + Db::name('cert_deploy')->where('id', $id)->delete(); + return json(['code' => 0]); + } elseif ($action == 'setactive') { + $id = input('post.id/d'); + $active = input('post.active/d'); + Db::name('cert_deploy')->where('id', $id)->update(['active' => $active]); + return json(['code' => 0]); + } elseif ($action == 'reset') { + $id = input('post.id/d'); + try { + $service = new CertDeployService($id); + $service->reset(); + return json(['code' => 0]); + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } elseif ($action == 'show_log') { + $processid = input('post.processid'); + $file = app()->getRuntimePath() . 'log/' . $processid . '.log'; + if (!file_exists($file)) return json(['code' => -1, 'msg' => '日志文件不存在']); + return json(['code' => 0, 'data' => file_get_contents($file), 'time' => filemtime($file)]); + } elseif ($action == 'operation') { + $ids = input('post.ids'); + $success = 0; + $certid = 0; + if (input('post.action') == 'cert') { + $certid = input('post.certid/d'); + $cert = Db::name('cert_order')->where('id', $certid)->find(); + if (!$cert) return json(['code' => -1, 'msg' => '证书订单不存在']); + } + foreach ($ids as $id) { + if (input('post.act') == 'delete') { + Db::name('cert_deploy')->where('id', $id)->delete(); + $success++; + } elseif (input('post.act') == 'reset') { + try { + $service = new CertDeployService($id); + $service->reset(); + $success++; + } catch (Exception $e) { + } + } elseif (input('post.act') == 'open' || input('post.act') == 'close') { + $active = input('post.act') == 'open' ? 1 : 0; + Db::name('cert_deploy')->where('id', $id)->update(['active' => $active]); + $success++; + } elseif (input('post.act') == 'cert') { + Db::name('cert_deploy')->where('id', $id)->update(['oid' => $certid]); + $success++; + } + } + return json(['code' => 0, 'msg' => '成功操作' . $success . '个任务']); + } + return json(['code' => -3]); + } + + public function deploy_process() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + if (function_exists("set_time_limit")) { + @set_time_limit(0); + } + if (function_exists("ignore_user_abort")) { + @ignore_user_abort(true); + } + $id = input('post.id/d'); + $reset = input('post.reset/d', 0); + try { + $service = new CertDeployService($id); + if ($reset == 1) { + $service->reset(); + } + $service->process(true); + return json(['code' => 0, 'msg' => 'SSL证书部署任务执行成功!']); + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage(), 'trace' => $e->getTrace()]); + } + } + + public function deploy_form() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + + $task = null; + if ($action == 'edit') { + $id = input('get.id/d'); + $task = Db::name('cert_deploy')->alias('A')->join('cert_account B', 'A.aid = B.id')->where('A.id', $id)->fieldRaw('A.id,A.aid,A.oid,A.config,A.remark,B.type')->find(); + if (empty($task)) return $this->alert('error', '自动部署任务不存在'); + } + + $accounts = []; + foreach (Db::name('cert_account')->where('deploy', 1)->select() as $row) { + if (empty($row['type']) || !isset(DeployHelper::$deploy_config[$row['type']])) continue; + $accounts[$row['id']] = ['name' => $row['id'] . '_' . DeployHelper::$deploy_config[$row['type']]['name'], 'type' => $row['type']]; + if (!empty($row['remark'])) { + $accounts[$row['id']]['name'] .= '(' . $row['remark'] . ')'; + } + } + View::assign('accounts', $accounts); + + $orders = []; + foreach (Db::name('cert_order')->alias('A')->leftJoin('cert_account B', 'A.aid = B.id')->where('status', '<>', 4)->fieldRaw('A.id,A.aid,B.type,B.remark aremark')->order('id', 'desc')->select() as $row) { + $domains = Db::name('cert_domain')->where('oid', $row['id'])->order('sort', 'ASC')->column('domain'); + $domainstr = count($domains) > 2 ? implode('、', array_slice($domains, 0, 2)) . '等' . count($domains) . '个域名' : implode('、', $domains); + if ($row['aid'] == 0) { + $name = $row['id'] . '_' . $domainstr . '(手动续期)'; + } else { + $name = $row['id'] . '_' . $domainstr . '(' . CertHelper::$cert_config[$row['type']]['name'] . ')'; + } + $orders[$row['id']] = ['name' => $name]; + } + View::assign('orders', $orders); + + View::assign('info', $task); + View::assign('action', $action); + View::assign('typeList', DeployHelper::getList()); + return View::fetch(); + } + + public function cname() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $domains = []; + foreach (Db::name('domain')->field('id,name')->select() as $row) { + $domains[$row['id']] = $row['name']; + } + View::assign('domains', $domains); + return view(); + } + + public function cname_data() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $kw = $this->request->post('kw', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('cert_cname')->alias('A')->leftJoin('domain B', 'A.did = B.id'); + if (!empty($kw)) { + $select->whereLike('A.domain', '%' . $kw . '%'); + } + $total = $select->count(); + $rows = $select->order('A.id', 'desc')->limit($offset, $limit)->field('A.*,B.name cnamedomain')->select(); + + $list = []; + foreach ($rows as $row) { + $row['host'] = $this->getCnameHost($row['domain']); + $row['record'] = $row['rr'] . '.' . $row['cnamedomain']; + $list[] = $row; + } + + return json(['total' => $total, 'rows' => $list]); + } + + private function getCnameHost($domain) + { + $main = getMainDomain($domain); + if ($main == $domain) { + return '_acme-challenge'; + } else { + return '_acme-challenge.' . substr($domain, 0, -strlen($main) - 1); + } + } + + public function cname_op() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + + if ($action == 'add') { + $data = [ + 'domain' => input('post.domain', null, 'trim'), + 'rr' => input('post.rr', null, 'trim'), + 'did' => input('post.did/d'), + 'addtime' => date('Y-m-d H:i:s'), + 'status' => 0 + ]; + if (empty($data['domain']) || empty($data['rr']) || empty($data['did'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + if (!checkDomain($data['domain'])) return json(['code' => -1, 'msg' => '域名格式不正确']); + if (Db::name('cert_cname')->where('domain', $data['domain'])->find()) { + return json(['code' => -1, 'msg' => '域名' . $data['domain'] . '已存在']); + } + if (Db::name('cert_cname')->where('rr', $data['rr'])->where('did', $data['did'])->find()) { + return json(['code' => -1, 'msg' => '已存在相同CNAME记录值']); + } + Db::name('cert_cname')->insert($data); + return json(['code' => 0, 'msg' => '添加CMAME代理成功!']); + } elseif ($action == 'edit') { + $id = input('post.id/d'); + $row = Db::name('cert_cname')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => 'CMAME代理不存在']); + + $data = [ + 'rr' => input('post.rr', null, 'trim'), + 'did' => input('post.did/d'), + ]; + if ($row['rr'] != $data['rr'] || $row['did'] != $data['did']) { + $data['status'] = 0; + } + if (empty($data['rr']) || empty($data['did'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + if (Db::name('cert_cname')->where('rr', $data['rr'])->where('did', $data['did'])->where('id', '<>', $id)->find()) { + return json(['code' => -1, 'msg' => '已存在相同CNAME记录值']); + } + Db::name('cert_cname')->where('id', $id)->update($data); + return json(['code' => 0, 'msg' => '修改CMAME代理成功!']); + } elseif ($action == 'del') { + $id = input('post.id/d'); + Db::name('cert_cname')->where('id', $id)->delete(); + return json(['code' => 0]); + } elseif ($action == 'check') { + $id = input('post.id/d'); + $row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.id', $id)->field('A.*,B.name cnamedomain')->find(); + if (!$row) return json(['code' => -1, 'msg' => '自动部署任务不存在']); + + $status = 1; + $domain = '_acme-challenge.' . $row['domain']; + $record = $row['rr'] . '.' . $row['cnamedomain']; + $result = \app\utils\DnsQueryUtils::get_dns_records($domain, 'CNAME'); + if (!$result || !in_array($record, $result)) { + $result = \app\utils\DnsQueryUtils::query_dns_doh($domain, 'CNAME'); + if (!$result || !in_array($record, $result)) { + $status = 0; + } + } + if ($status != $row['status']) { + Db::name('cert_cname')->where('id', $id)->update(['status' => $status]); + } + return json(['code' => 0, 'status' => $status]); + } + } + + public function certset() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + return View::fetch(); + } +} diff --git a/app/controller/Dmonitor.php b/app/controller/Dmonitor.php index c69b0a7..8b0b617 100644 --- a/app/controller/Dmonitor.php +++ b/app/controller/Dmonitor.php @@ -1,266 +1,266 @@ -alert('error', '无权限'); - $switch_count = Db::name('dmlog')->where('date', '>=', date("Y-m-d H:i:s", strtotime("-1 days")))->count(); - $fail_count = Db::name('dmlog')->where('date', '>=', date("Y-m-d H:i:s", strtotime("-1 days")))->where('action', 1)->count(); - - $run_time = config_get('run_time', null, true); - $run_state = $run_time ? (time() - strtotime($run_time) > 10 ? 0 : 1) : 0; - View::assign('info', [ - 'run_count' => config_get('run_count', null, true) ?? 0, - 'run_time' => $run_time ?? '无', - 'run_state' => $run_state, - 'run_error' => config_get('run_error', null, true), - 'switch_count' => $switch_count, - 'fail_count' => $fail_count, - 'swoole' => extension_loaded('swoole') ? '已安装' : '未安装', - ]); - return View::fetch(); - } - - public function task() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - return View::fetch(); - } - - public function task_data() - { - if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); - $type = input('post.type/d', 1); - $status = input('post.status', null); - $kw = input('post.kw', null, 'trim'); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - - $select = Db::name('dmtask')->alias('A')->join('domain B', 'A.did = B.id'); - if (!empty($kw)) { - if ($type == 1) { - $select->whereLike('rr|B.name', '%' . $kw . '%'); - } elseif ($type == 2) { - $select->where('recordid', $kw); - } elseif ($type == 3) { - $select->where('main_value', $kw); - } elseif ($type == 4) { - $select->where('backup_value', $kw); - } elseif ($type == 5) { - $select->whereLike('remark', '%' . $kw . '%'); - } - } - if (!isNullOrEmpty($status)) { - $select->where('status', intval($status)); - } - $total = $select->count(); - $list = $select->order('A.id', 'desc')->limit($offset, $limit)->field('A.*,B.name domain')->select()->toArray(); - - foreach ($list as &$row) { - $row['addtimestr'] = date('Y-m-d H:i:s', $row['addtime']); - $row['checktimestr'] = $row['checktime'] > 0 ? date('Y-m-d H:i:s', $row['checktime']) : '未运行'; - } - - return json(['total' => $total, 'rows' => $list]); - } - - public function task_op() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - if ($action == 'add') { - $task = [ - 'did' => input('post.did/d'), - 'rr' => input('post.rr', null, 'trim'), - 'recordid' => input('post.recordid', null, 'trim'), - 'type' => input('post.type/d'), - 'main_value' => input('post.main_value', null, 'trim'), - 'backup_value' => input('post.backup_value', null, 'trim'), - 'checktype' => input('post.checktype/d'), - 'checkurl' => input('post.checkurl', null, 'trim'), - 'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null, - 'frequency' => input('post.frequency/d'), - 'cycle' => input('post.cycle/d'), - 'timeout' => input('post.timeout/d'), - 'proxy' => input('post.proxy/d'), - 'cdn' => input('post.cdn') == 'true' || input('post.cdn') == '1' ? 1 : 0, - 'remark' => input('post.remark', null, 'trim'), - 'recordinfo' => input('post.recordinfo', null, 'trim'), - 'addtime' => time(), - 'active' => 1 - ]; - - if (empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])) { - return json(['code' => -1, 'msg' => '必填项不能为空']); - } - if ($task['checktype'] > 0 && $task['timeout'] > $task['frequency']) { - return json(['code' => -1, 'msg' => '为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']); - } - if ($task['type'] == 2 && $task['backup_value'] == $task['main_value']) { - return json(['code' => -1, 'msg' => '主备地址不能相同']); - } - if (Db::name('dmtask')->where('recordid', $task['recordid'])->find()) { - return json(['code' => -1, 'msg' => '当前容灾切换策略已存在']); - } - Db::name('dmtask')->insert($task); - return json(['code' => 0, 'msg' => '添加成功']); - } elseif ($action == 'edit') { - $id = input('post.id/d'); - $task = [ - 'did' => input('post.did/d'), - 'rr' => input('post.rr', null, 'trim'), - 'recordid' => input('post.recordid', null, 'trim'), - 'type' => input('post.type/d'), - 'main_value' => input('post.main_value', null, 'trim'), - 'backup_value' => input('post.backup_value', null, 'trim'), - 'checktype' => input('post.checktype/d'), - 'checkurl' => input('post.checkurl', null, 'trim'), - 'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null, - 'frequency' => input('post.frequency/d'), - 'cycle' => input('post.cycle/d'), - 'timeout' => input('post.timeout/d'), - 'proxy' => input('post.proxy/d'), - 'cdn' => input('post.cdn') == 'true' || input('post.cdn') == '1' ? 1 : 0, - 'remark' => input('post.remark', null, 'trim'), - 'recordinfo' => input('post.recordinfo', null, 'trim'), - ]; - - if (empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])) { - return json(['code' => -1, 'msg' => '必填项不能为空']); - } - if ($task['checktype'] > 0 && $task['timeout'] > $task['frequency']) { - return json(['code' => -1, 'msg' => '为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']); - } - if ($task['type'] == 2 && $task['backup_value'] == $task['main_value']) { - return json(['code' => -1, 'msg' => '主备地址不能相同']); - } - if (Db::name('dmtask')->where('recordid', $task['recordid'])->where('id', '<>', $id)->find()) { - return json(['code' => -1, 'msg' => '当前容灾切换策略已存在']); - } - Db::name('dmtask')->where('id', $id)->update($task); - return json(['code' => 0, 'msg' => '修改成功']); - } elseif ($action == 'setactive') { - $id = input('post.id/d'); - $active = input('post.active/d'); - Db::name('dmtask')->where('id', $id)->update(['active' => $active]); - return json(['code' => 0, 'msg' => '设置成功']); - } elseif ($action == 'del') { - $id = input('post.id/d'); - Db::name('dmtask')->where('id', $id)->delete(); - Db::name('dmlog')->where('taskid', $id)->delete(); - return json(['code' => 0, 'msg' => '删除成功']); - } elseif ($action == 'operation') { - $ids = input('post.ids'); - $success = 0; - foreach ($ids as $id) { - if (input('post.act') == 'delete') { - Db::name('dmtask')->where('id', $id)->delete(); - Db::name('dmlog')->where('taskid', $id)->delete(); - $success++; - } elseif (input('post.act') == 'retry') { - Db::name('dmtask')->where('id', $id)->update(['checknexttime' => time()]); - $success++; - } elseif (input('post.act') == 'open' || input('post.act') == 'close') { - $isauto = input('post.act') == 'open' ? 1 : 0; - Db::name('dmtask')->where('id', $id)->update(['active' => $isauto]); - $success++; - } - } - return json(['code' => 0, 'msg' => '成功操作' . $success . '个容灾切换策略']); - } else { - return json(['code' => -1, 'msg' => '参数错误']); - } - } - - public function taskform() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - $task = null; - if ($action == 'edit') { - $id = input('get.id/d'); - $task = Db::name('dmtask')->where('id', $id)->find(); - if (empty($task)) return $this->alert('error', '切换策略不存在'); - } - - $domains = []; - $domainList = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->field('A.id,A.name,B.type')->select(); - foreach ($domainList as $row) { - $domains[] = ['id'=>$row['id'], 'name'=>$row['name'], 'type'=>$row['type']]; - } - View::assign('domains', $domains); - - View::assign('info', $task); - View::assign('action', $action); - View::assign('support_ping', function_exists('exec') ? '1' : '0'); - return View::fetch(); - } - - public function taskinfo() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $id = input('param.id/d'); - $task = Db::name('dmtask')->where('id', $id)->find(); - if (empty($task)) return $this->alert('error', '切换策略不存在'); - - $switch_count = Db::name('dmlog')->where('taskid', $id)->where('date', '>=', date("Y-m-d H:i:s", strtotime("-1 days")))->count(); - $fail_count = Db::name('dmlog')->where('taskid', $id)->where('date', '>=', date("Y-m-d H:i:s", strtotime("-1 days")))->where('action', 1)->count(); - - $task['switch_count'] = $switch_count; - $task['fail_count'] = $fail_count; - if ($task['type'] == 3) { - $task['action_name'] = ['未知', '开启解析', '暂停解析']; - } elseif ($task['type'] == 2) { - $task['action_name'] = ['未知', '切换备用解析记录', '恢复主解析记录']; - } else { - $task['action_name'] = ['未知', '暂停解析', '启用解析']; - } - View::assign('info', $task); - return View::fetch(); - } - - public function tasklog_data() - { - if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); - $taskid = input('param.id/d'); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - $action = input('post.action/d', 0); - - $select = Db::name('dmlog')->where('taskid', $taskid); - if ($action > 0) { - $select->where('action', $action); - } - $total = $select->count(); - $list = $select->order('id', 'desc')->limit($offset, $limit)->select(); - - return json(['total' => $total, 'rows' => $list]); - } - - public function clean() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - if ($this->request->isPost()) { - $days = input('post.days/d'); - if (!$days || $days < 0) return json(['code' => -1, 'msg' => '参数错误']); - Db::execute("DELETE FROM `" . config('database.connections.mysql.prefix') . "dmlog` WHERE `date`<'" . date("Y-m-d H:i:s", strtotime("-" . $days . " days")) . "'"); - Db::execute("OPTIMIZE TABLE `" . config('database.connections.mysql.prefix') . "dmlog`"); - return json(['code' => 0, 'msg' => '清理成功']); - } - } - - public function status() - { - $run_time = config_get('run_time', null, true); - $run_state = $run_time ? (time() - strtotime($run_time) > 10 ? 0 : 1) : 0; - return $run_state == 1 ? 'ok' : 'error'; - } -} +alert('error', '无权限'); + $switch_count = Db::name('dmlog')->where('date', '>=', date("Y-m-d H:i:s", strtotime("-1 days")))->count(); + $fail_count = Db::name('dmlog')->where('date', '>=', date("Y-m-d H:i:s", strtotime("-1 days")))->where('action', 1)->count(); + + $run_time = config_get('run_time', null, true); + $run_state = $run_time ? (time() - strtotime($run_time) > 10 ? 0 : 1) : 0; + View::assign('info', [ + 'run_count' => config_get('run_count', null, true) ?? 0, + 'run_time' => $run_time ?? '无', + 'run_state' => $run_state, + 'run_error' => config_get('run_error', null, true), + 'switch_count' => $switch_count, + 'fail_count' => $fail_count, + 'swoole' => extension_loaded('swoole') ? '已安装' : '未安装', + ]); + return View::fetch(); + } + + public function task() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + return View::fetch(); + } + + public function task_data() + { + if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); + $type = input('post.type/d', 1); + $status = input('post.status', null); + $kw = input('post.kw', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('dmtask')->alias('A')->join('domain B', 'A.did = B.id'); + if (!empty($kw)) { + if ($type == 1) { + $select->whereLike('rr|B.name', '%' . $kw . '%'); + } elseif ($type == 2) { + $select->where('recordid', $kw); + } elseif ($type == 3) { + $select->where('main_value', $kw); + } elseif ($type == 4) { + $select->where('backup_value', $kw); + } elseif ($type == 5) { + $select->whereLike('remark', '%' . $kw . '%'); + } + } + if (!isNullOrEmpty($status)) { + $select->where('status', intval($status)); + } + $total = $select->count(); + $list = $select->order('A.id', 'desc')->limit($offset, $limit)->field('A.*,B.name domain')->select()->toArray(); + + foreach ($list as &$row) { + $row['addtimestr'] = date('Y-m-d H:i:s', $row['addtime']); + $row['checktimestr'] = $row['checktime'] > 0 ? date('Y-m-d H:i:s', $row['checktime']) : '未运行'; + } + + return json(['total' => $total, 'rows' => $list]); + } + + public function task_op() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + if ($action == 'add') { + $task = [ + 'did' => input('post.did/d'), + 'rr' => input('post.rr', null, 'trim'), + 'recordid' => input('post.recordid', null, 'trim'), + 'type' => input('post.type/d'), + 'main_value' => input('post.main_value', null, 'trim'), + 'backup_value' => input('post.backup_value', null, 'trim'), + 'checktype' => input('post.checktype/d'), + 'checkurl' => input('post.checkurl', null, 'trim'), + 'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null, + 'frequency' => input('post.frequency/d'), + 'cycle' => input('post.cycle/d'), + 'timeout' => input('post.timeout/d'), + 'proxy' => input('post.proxy/d'), + 'cdn' => input('post.cdn') == 'true' || input('post.cdn') == '1' ? 1 : 0, + 'remark' => input('post.remark', null, 'trim'), + 'recordinfo' => input('post.recordinfo', null, 'trim'), + 'addtime' => time(), + 'active' => 1 + ]; + + if (empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])) { + return json(['code' => -1, 'msg' => '必填项不能为空']); + } + if ($task['checktype'] > 0 && $task['timeout'] > $task['frequency']) { + return json(['code' => -1, 'msg' => '为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']); + } + if ($task['type'] == 2 && $task['backup_value'] == $task['main_value']) { + return json(['code' => -1, 'msg' => '主备地址不能相同']); + } + if (Db::name('dmtask')->where('recordid', $task['recordid'])->find()) { + return json(['code' => -1, 'msg' => '当前容灾切换策略已存在']); + } + Db::name('dmtask')->insert($task); + return json(['code' => 0, 'msg' => '添加成功']); + } elseif ($action == 'edit') { + $id = input('post.id/d'); + $task = [ + 'did' => input('post.did/d'), + 'rr' => input('post.rr', null, 'trim'), + 'recordid' => input('post.recordid', null, 'trim'), + 'type' => input('post.type/d'), + 'main_value' => input('post.main_value', null, 'trim'), + 'backup_value' => input('post.backup_value', null, 'trim'), + 'checktype' => input('post.checktype/d'), + 'checkurl' => input('post.checkurl', null, 'trim'), + 'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null, + 'frequency' => input('post.frequency/d'), + 'cycle' => input('post.cycle/d'), + 'timeout' => input('post.timeout/d'), + 'proxy' => input('post.proxy/d'), + 'cdn' => input('post.cdn') == 'true' || input('post.cdn') == '1' ? 1 : 0, + 'remark' => input('post.remark', null, 'trim'), + 'recordinfo' => input('post.recordinfo', null, 'trim'), + ]; + + if (empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])) { + return json(['code' => -1, 'msg' => '必填项不能为空']); + } + if ($task['checktype'] > 0 && $task['timeout'] > $task['frequency']) { + return json(['code' => -1, 'msg' => '为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']); + } + if ($task['type'] == 2 && $task['backup_value'] == $task['main_value']) { + return json(['code' => -1, 'msg' => '主备地址不能相同']); + } + if (Db::name('dmtask')->where('recordid', $task['recordid'])->where('id', '<>', $id)->find()) { + return json(['code' => -1, 'msg' => '当前容灾切换策略已存在']); + } + Db::name('dmtask')->where('id', $id)->update($task); + return json(['code' => 0, 'msg' => '修改成功']); + } elseif ($action == 'setactive') { + $id = input('post.id/d'); + $active = input('post.active/d'); + Db::name('dmtask')->where('id', $id)->update(['active' => $active]); + return json(['code' => 0, 'msg' => '设置成功']); + } elseif ($action == 'del') { + $id = input('post.id/d'); + Db::name('dmtask')->where('id', $id)->delete(); + Db::name('dmlog')->where('taskid', $id)->delete(); + return json(['code' => 0, 'msg' => '删除成功']); + } elseif ($action == 'operation') { + $ids = input('post.ids'); + $success = 0; + foreach ($ids as $id) { + if (input('post.act') == 'delete') { + Db::name('dmtask')->where('id', $id)->delete(); + Db::name('dmlog')->where('taskid', $id)->delete(); + $success++; + } elseif (input('post.act') == 'retry') { + Db::name('dmtask')->where('id', $id)->update(['checknexttime' => time()]); + $success++; + } elseif (input('post.act') == 'open' || input('post.act') == 'close') { + $isauto = input('post.act') == 'open' ? 1 : 0; + Db::name('dmtask')->where('id', $id)->update(['active' => $isauto]); + $success++; + } + } + return json(['code' => 0, 'msg' => '成功操作' . $success . '个容灾切换策略']); + } else { + return json(['code' => -1, 'msg' => '参数错误']); + } + } + + public function taskform() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + $task = null; + if ($action == 'edit') { + $id = input('get.id/d'); + $task = Db::name('dmtask')->where('id', $id)->find(); + if (empty($task)) return $this->alert('error', '切换策略不存在'); + } + + $domains = []; + $domainList = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->field('A.id,A.name,B.type')->select(); + foreach ($domainList as $row) { + $domains[] = ['id'=>$row['id'], 'name'=>$row['name'], 'type'=>$row['type']]; + } + View::assign('domains', $domains); + + View::assign('info', $task); + View::assign('action', $action); + View::assign('support_ping', function_exists('exec') ? '1' : '0'); + return View::fetch(); + } + + public function taskinfo() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $id = input('param.id/d'); + $task = Db::name('dmtask')->where('id', $id)->find(); + if (empty($task)) return $this->alert('error', '切换策略不存在'); + + $switch_count = Db::name('dmlog')->where('taskid', $id)->where('date', '>=', date("Y-m-d H:i:s", strtotime("-1 days")))->count(); + $fail_count = Db::name('dmlog')->where('taskid', $id)->where('date', '>=', date("Y-m-d H:i:s", strtotime("-1 days")))->where('action', 1)->count(); + + $task['switch_count'] = $switch_count; + $task['fail_count'] = $fail_count; + if ($task['type'] == 3) { + $task['action_name'] = ['未知', '开启解析', '暂停解析']; + } elseif ($task['type'] == 2) { + $task['action_name'] = ['未知', '切换备用解析记录', '恢复主解析记录']; + } else { + $task['action_name'] = ['未知', '暂停解析', '启用解析']; + } + View::assign('info', $task); + return View::fetch(); + } + + public function tasklog_data() + { + if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); + $taskid = input('param.id/d'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + $action = input('post.action/d', 0); + + $select = Db::name('dmlog')->where('taskid', $taskid); + if ($action > 0) { + $select->where('action', $action); + } + $total = $select->count(); + $list = $select->order('id', 'desc')->limit($offset, $limit)->select(); + + return json(['total' => $total, 'rows' => $list]); + } + + public function clean() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + if ($this->request->isPost()) { + $days = input('post.days/d'); + if (!$days || $days < 0) return json(['code' => -1, 'msg' => '参数错误']); + Db::execute("DELETE FROM `" . config('database.connections.mysql.prefix') . "dmlog` WHERE `date`<'" . date("Y-m-d H:i:s", strtotime("-" . $days . " days")) . "'"); + Db::execute("OPTIMIZE TABLE `" . config('database.connections.mysql.prefix') . "dmlog`"); + return json(['code' => 0, 'msg' => '清理成功']); + } + } + + public function status() + { + $run_time = config_get('run_time', null, true); + $run_state = $run_time ? (time() - strtotime($run_time) > 10 ? 0 : 1) : 0; + return $run_state == 1 ? 'ok' : 'error'; + } +} diff --git a/app/controller/Domain.php b/app/controller/Domain.php index 85c7df4..03e04a0 100644 --- a/app/controller/Domain.php +++ b/app/controller/Domain.php @@ -1,1099 +1,1099 @@ -alert('error', '无权限'); - View::assign('dnsconfig', DnsHelper::$dns_config); - return view(); - } - - public function account_data() - { - if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); - $kw = $this->request->post('kw', null, 'trim'); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - - $select = Db::name('account'); - if (!empty($kw)) { - $select->whereLike('ak|remark', '%' . $kw . '%'); - } - $total = $select->count(); - $rows = $select->order('id', 'desc')->limit($offset, $limit)->select(); - - $list = []; - foreach ($rows as $row) { - $row['typename'] = DnsHelper::$dns_config[$row['type']]['name']; - $list[] = $row; - } - - return json(['total' => $total, 'rows' => $list]); - } - - 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') { - $type = input('post.type'); - $ak = input('post.ak', null, 'trim'); - $sk = input('post.sk', null, 'trim'); - $ext = input('post.ext', 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()) { - return json(['code' => -1, 'msg' => '域名账户已存在']); - } - Db::startTrans(); - $id = Db::name('account')->insertGetId([ - 'type' => $type, - 'ak' => $ak, - 'sk' => $sk, - 'ext' => $ext, - 'proxy' => $proxy, - 'remark' => $remark, - 'addtime' => date('Y-m-d H:i:s'), - ]); - $dns = DnsHelper::getModel($id); - if ($dns) { - if ($dns->check()) { - Db::commit(); - return json(['code' => 0, 'msg' => '添加域名账户成功!']); - } else { - Db::rollback(); - return json(['code' => -1, 'msg' => '验证域名账户失败,' . $dns->getError()]); - } - } else { - Db::rollback(); - return json(['code' => -1, 'msg' => 'DNS模块(' . $type . ')不存在']); - } - } elseif ($act == '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'); - $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()) { - return json(['code' => -1, 'msg' => '域名账户已存在']); - } - Db::startTrans(); - Db::name('account')->where('id', $id)->update([ - 'type' => $type, - 'ak' => $ak, - 'sk' => $sk, - 'ext' => $ext, - 'proxy' => $proxy, - 'remark' => $remark, - ]); - $dns = DnsHelper::getModel($id); - if ($dns) { - if ($dns->check()) { - Db::commit(); - return json(['code' => 0, 'msg' => '修改域名账户成功!']); - } else { - Db::rollback(); - return json(['code' => -1, 'msg' => '验证域名账户失败,' . $dns->getError()]); - } - } else { - Db::rollback(); - return json(['code' => -1, 'msg' => 'DNS模块(' . $type . ')不存在']); - } - } elseif ($act == 'del') { - $id = input('post.id/d'); - $dcount = DB::name('domain')->where('aid', $id)->count(); - if ($dcount > 0) return json(['code' => -1, 'msg' => '该域名账户下存在域名,无法删除']); - Db::name('account')->where('id', $id)->delete(); - return json(['code' => 0]); - } - return json(['code' => -3]); - } - - - public function domain() - { - if (request()->user['type'] == 'domain') { - return redirect('/record/' . request()->user['id']); - } - $list = Db::name('account')->select(); - $accounts = []; - $types = []; - foreach ($list as $row) { - $name = $row['id'] . '_' . DnsHelper::$dns_config[$row['type']]['name']; - if (!array_key_exists($row['type'], $types)) { - $types[$row['type']] = DnsHelper::$dns_config[$row['type']]['name']; - } - if (!empty($row['remark'])) { - $name .= '(' . $row['remark'] . ')'; - } - $accounts[] = ['id' => $row['id'], 'name' => $name, 'type' => DnsHelper::$dns_config[$row['type']]['name'], 'add' => DnsHelper::$dns_config[$row['type']]['add']]; - } - View::assign('accounts', $accounts); - View::assign('types', $types); - return view(); - } - - public function domain_add() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $list = Db::name('account')->select(); - $accounts = []; - $types = []; - foreach ($list as $row) { - $accounts[$row['id']] = $row['id'] . '_' . DnsHelper::$dns_config[$row['type']]['name']; - if (!array_key_exists($row['type'], $types)) { - $types[$row['type']] = DnsHelper::$dns_config[$row['type']]['name']; - } - if (!empty($row['remark'])) { - $accounts[$row['id']] .= '(' . $row['remark'] . ')'; - } - } - View::assign('accounts', $accounts); - View::assign('types', $types); - return view(); - } - - public function domain_data() - { - if (!checkPermission(1)) return json(['total' => 0, 'rows' => []]); - $kw = input('post.kw', null, 'trim'); - $type = input('post.type', null, 'trim'); - $status = input('post.status', null, 'trim'); - $offset = input('post.offset/d', 0); - $limit = input('post.limit/d', 10); - - $select = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id'); - if (!empty($kw)) { - $select->whereLike('name|A.remark', '%' . $kw . '%'); - } - if (!empty($type)) { - $select->whereLike('B.type', $type); - } - if (request()->user['level'] == 1) { - $select->where('is_hide', 0)->where('A.name', 'in', request()->user['permission']); - } - if (!isNullOrEmpty($status)) { - if ($status == '2') { - $select->where('A.expiretime', '<=', date('Y-m-d H:i:s')); - } elseif ($status == '1') { - $select->where('A.expiretime', '<=', date('Y-m-d H:i:s', time() + 86400 * 30))->where('A.expiretime', '>', date('Y-m-d H:i:s')); - } - } - $total = $select->count(); - $rows = $select->fieldRaw('A.*,B.type,B.remark aremark')->order('A.id', 'desc')->limit($offset, $limit)->select(); - - $list = []; - foreach ($rows as $row) { - $row['typename'] = DnsHelper::$dns_config[$row['type']]['name']; - $list[] = $row; - } - - return json(['total' => $total, 'rows' => $list]); - } - - public function domain_op() - { - if (!checkPermission(1)) return $this->alert('error', '无权限'); - $act = input('param.act'); - if ($act == 'get') { - $id = input('post.id/d'); - $row = Db::name('domain')->where('id', $id)->find(); - if (!$row) return json(['code' => -1, 'msg' => '域名不存在']); - return json(['code' => 0, 'data' => $row]); - } elseif ($act == 'add') { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $aid = input('post.aid/d'); - $method = input('post.method/d', 0); - $name = input('post.name', null, 'trim'); - $thirdid = input('post.thirdid', null, 'trim'); - $recordcount = input('post.recordcount/d', 0); - if ($method == 1 && empty($name) || $method == 0 && (empty($name) || empty($thirdid))) return json(['code' => -1, 'msg' => '参数不能为空']); - if (Db::name('domain')->where('aid', $aid)->where('name', $name)->find()) { - return json(['code' => -1, 'msg' => '域名已存在']); - } - if ($method == 1) { - $dns = DnsHelper::getModel($aid); - $result = $dns->addDomain($name); - if (!$result) return json(['code' => -1, 'msg' => '添加域名失败,' . $dns->getError()]); - $name = $result['name']; - $thirdid = $result['id']; - } - Db::name('domain')->insert([ - 'aid' => $aid, - 'name' => $name, - 'thirdid' => $thirdid, - 'addtime' => date('Y-m-d H:i:s'), - 'is_hide' => 0, - 'is_sso' => 1, - 'recordcount' => $recordcount, - ]); - return json(['code' => 0, 'msg' => '添加域名成功!']); - } elseif ($act == 'edit') { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $id = input('post.id/d'); - $row = Db::name('domain')->where('id', $id)->find(); - if (!$row) return json(['code' => -1, 'msg' => '域名不存在']); - $is_hide = input('post.is_hide/d'); - $is_sso = input('post.is_sso/d'); - $is_notice = input('post.is_notice/d'); - $expiretime = input('post.expiretime', null, 'trim'); - $remark = input('post.remark', null, 'trim'); - if (empty($remark)) $remark = null; - Db::name('domain')->where('id', $id)->update([ - 'is_hide' => $is_hide, - 'is_sso' => $is_sso, - 'is_notice' => $is_notice, - 'expiretime' => $expiretime ? $expiretime : null, - 'remark' => $remark, - ]); - return json(['code' => 0, 'msg' => '修改域名配置成功!']); - } elseif ($act == 'del') { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $id = input('post.id/d'); - Db::name('domain')->where('id', $id)->delete(); - Db::name('dmtask')->where('did', $id)->delete(); - Db::name('optimizeip')->where('did', $id)->delete(); - Db::name('sctask')->where('did', $id)->delete(); - return json(['code' => 0]); - } elseif ($act == 'batchadd') { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $aid = input('post.aid/d'); - $domains = input('post.domains'); - if (empty($domains)) return json(['code' => -1, 'msg' => '参数不能为空']); - $data = []; - foreach ($domains as $row) { - $data[] = [ - 'aid' => $aid, - 'name' => $row['name'], - 'thirdid' => $row['id'], - 'addtime' => date('Y-m-d H:i:s'), - 'is_hide' => 0, - 'is_sso' => 1, - 'recordcount' => $row['recordcount'], - ]; - } - Db::name('domain')->insertAll($data); - return json(['code' => 0, 'msg' => '成功添加' . count($data) . '个域名!']); - } elseif ($act == 'batchedit') { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $ids = input('post.ids'); - if (empty($ids)) return json(['code' => -1, 'msg' => '参数不能为空']); - $remark = input('post.remark', null, 'trim'); - if (empty($remark)) $remark = null; - $count = Db::name('domain')->where('id', 'in', $ids)->update(['remark' => $remark]); - return json(['code' => 0, 'msg' => '成功修改' . $count . '个域名!']); - } elseif ($act == 'batchsetnotice') { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $ids = input('post.ids'); - $is_notice = input('post.is_notice/d', 0); - if (empty($ids)) return json(['code' => -1, 'msg' => '参数不能为空']); - $count = Db::name('domain')->where('id', 'in', $ids)->update(['is_notice' => $is_notice]); - return json(['code' => 0, 'msg' => '成功修改' . $count . '个域名!']); - } elseif ($act == 'batchdel') { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $ids = input('post.ids'); - if (empty($ids)) return json(['code' => -1, 'msg' => '参数不能为空']); - Db::name('domain')->where('id', 'in', $ids)->delete(); - Db::name('dmtask')->where('did', 'in', $ids)->delete(); - Db::name('optimizeip')->where('did', 'in', $ids)->delete(); - Db::name('sctask')->where('did', 'in', $ids)->delete(); - return json(['code' => 0, 'msg' => '成功删除' . count($ids) . '个域名!']); - } - return json(['code' => -3]); - } - - public function domain_list() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $aid = input('post.aid/d'); - $kw = input('post.kw', null, 'trim'); - $page = input('?post.page') ? input('post.page/d') : 1; - $pagesize = input('?post.pagesize') ? input('post.pagesize/d') : 10; - $dns = DnsHelper::getModel($aid); - $result = $dns->getDomainList($kw, $page, $pagesize); - if (!$result) return json(['code' => -1, 'msg' => '获取域名列表失败,' . $dns->getError()]); - - foreach ($result['list'] as &$row) { - $row['disabled'] = Db::name('domain')->where('aid', $aid)->where('name', $row['Domain'])->find() != null; - } - return json(['code' => 0, 'data' => ['total' => $result['total'], 'list' => $result['list']]]); - } - - //获取解析线路和最小TTL - private function get_line_and_ttl($drow) - { - $recordLine = cache('record_line_' . $drow['id']); - $minTTL = cache('min_ttl_' . $drow['id']); - if (empty($recordLine)) { - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - if (!$dns) throw new Exception('DNS模块不存在'); - $recordLine = $dns->getRecordLine(); - if (!$recordLine) throw new Exception('获取解析线路列表失败,' . $dns->getError()); - cache('record_line_' . $drow['id'], $recordLine, 604800); - $minTTL = $dns->getMinTTL(); - if ($minTTL) { - cache('min_ttl_' . $drow['id'], $minTTL, 604800); - } - } - return [$recordLine, $minTTL]; - } - - public function domain_info() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return $this->alert('error', '域名不存在'); - } - $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - - list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); - - $recordLineArr = []; - foreach ($recordLine as $key => $item) { - $recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']]; - } - - $dnsconfig = DnsHelper::$dns_config[$dnstype]; - $dnsconfig['type'] = $dnstype; - - $drow['config'] = $dnsconfig; - $drow['recordLine'] = $recordLineArr; - $drow['minTTL'] = $minTTL ? $minTTL : 1; - if (input('?post.loginurl') && input('post.loginurl') == '1') { - $token = getSid(); - cache('quicklogin_' . $drow['name'], $token, 3600); - $timestamp = time(); - $sign = md5(config_get('sys_key') . $drow['name'] . $timestamp . $token . config_get('sys_key')); - $drow['loginurl'] = request()->root(true) . '/quicklogin?domain=' . $drow['name'] . '×tamp=' . $timestamp . '&token=' . $token . '&sign=' . $sign; - } - - return json(['code' => 0, 'data' => $drow]); - } - - public function record() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return $this->alert('error', '域名不存在'); - } - $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - - list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); - - $recordLineArr = []; - foreach ($recordLine as $key => $item) { - $recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']]; - } - - $dnsconfig = DnsHelper::$dns_config[$dnstype]; - $dnsconfig['type'] = $dnstype; - - View::assign('domainId', $id); - View::assign('domainName', $drow['name']); - View::assign('recordLine', $recordLineArr); - View::assign('minTTL', $minTTL ? $minTTL : 1); - View::assign('dnsconfig', $dnsconfig); - return view(); - } - - public function record_data() - { - $id = input('param.id/d'); - $keyword = input('post.keyword', null, 'trim'); - $subdomain = input('post.subdomain', null, 'trim'); - $value = input('post.value', null, 'trim'); - $type = input('post.type', null, 'trim'); - $line = input('post.line', null, 'trim'); - $status = input('post.status', null, 'trim'); - $offset = input('post.offset/d', 0); - $limit = input('post.limit/d', 10); - if ($limit == 0) { - $page = 1; - } else { - $page = $offset / $limit + 1; - } - - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['total' => 0, 'rows' => []]); - } - if (!checkPermission(0, $drow['name'])) return json(['total' => 0, 'rows' => []]); - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - $domainRecords = $dns->getDomainRecords($page, $limit, $keyword, $subdomain, $value, $type, $line, $status); - if (!$domainRecords) return json(['total' => 0, 'rows' => []]); - - if (empty($keyword) && empty($subdomain) && empty($type) && isNullOrEmpty($line) && empty($status) && empty($value) && $domainRecords['total'] != $drow['recordcount']) { - Db::name('domain')->where('id', $id)->update(['recordcount' => $domainRecords['total']]); - } - - $recordLine = cache('record_line_' . $id); - - foreach ($domainRecords['list'] as &$row) { - $row['LineName'] = isset($recordLine[$row['Line']]) ? $recordLine[$row['Line']]['name'] : $row['Line']; - } - - $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); - if (DnsHelper::$dns_config[$dnstype]['page']) { - return json($domainRecords['list']); - } - - return json(['total' => $domainRecords['total'], 'rows' => $domainRecords['list']]); - } - - public function record_list() - { - $id = input('post.id/d'); - $rr = input('post.rr', null, 'trim'); - - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['code' => -1, 'msg' => '域名不存在']); - } - if (!checkPermission(0, $drow['name'])) return json(['code' => -1, 'msg' => '无权限']); - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - $domainRecords = $dns->getSubDomainRecords($rr, 1, 100); - if (!$domainRecords) return json(['code' => -1, 'msg' => '获取记录列表失败,' . $dns->getError()]); - - list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); - - $list = []; - foreach ($domainRecords['list'] as &$row) { - if ($rr == '@' && ($row['Type'] == 'NS' || $row['Type'] == 'SOA')) continue; - $row['LineName'] = isset($recordLine[$row['Line']]) ? $recordLine[$row['Line']]['name'] : $row['Line']; - $list[] = $row; - } - - return json(['code' => 0, 'data' => $list]); - } - - public function record_add() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['code' => -1, 'msg' => '域名不存在']); - } - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - - $name = input('post.name', null, 'trim'); - $type = input('post.type', null, 'trim'); - $value = input('post.value', null, 'trim'); - $line = input('post.line', null, 'trim'); - $ttl = input('post.ttl/d', 600); - $weight = input('post.weight/d', 0); - $mx = input('post.mx/d', 1); - $remark = input('post.remark', null, 'trim'); - - if (empty($name) || empty($type) || empty($value)) { - return json(['code' => -1, 'msg' => '参数不能为空']); - } - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - $recordid = $dns->addDomainRecord($name, $type, $value, $line, $ttl, $mx, $weight, $remark); - if ($recordid) { - $this->add_log($drow['name'], '添加解析', $name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')'); - return json(['code' => 0, 'msg' => '添加解析记录成功!']); - } else { - return json(['code' => -1, 'msg' => '添加解析记录失败,' . $dns->getError()]); - } - } - - public function record_update() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['code' => -1, 'msg' => '域名不存在']); - } - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - - $recordid = input('post.recordid', null, 'trim'); - $name = input('post.name', null, 'trim'); - $type = input('post.type', null, 'trim'); - $value = input('post.value', null, 'trim'); - $line = input('post.line', null, 'trim'); - $ttl = input('post.ttl/d', 600); - $weight = input('post.weight/d', 0); - $mx = input('post.mx/d', 1); - $remark = input('post.remark', null, 'trim'); - - $recordinfo = input('post.recordinfo', null, 'trim'); - - if (empty($recordid) || empty($name) || empty($type) || empty($value)) { - return json(['code' => -1, 'msg' => '参数不能为空']); - } - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - $recordid = $dns->updateDomainRecord($recordid, $name, $type, $value, $line, $ttl, $mx, $weight, $remark); - if ($recordid) { - if ($recordinfo) { - $recordinfo = json_decode($recordinfo, true); - if (is_array($recordinfo['Value'])) $recordinfo['Value'] = implode(',', $recordinfo['Value']); - if ($recordinfo['Name'] != $name || $recordinfo['Type'] != $type || $recordinfo['Value'] != $value) { - $this->add_log($drow['name'], '修改解析', $recordinfo['Name'].' ['.$recordinfo['Type'].'] '.$recordinfo['Value'].' → '.$name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')'); - } elseif($recordinfo['Line'] != $line || $recordinfo['TTL'] != $ttl) { - $this->add_log($drow['name'], '修改解析', $name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')'); - } - } else { - $this->add_log($drow['name'], '修改解析', $name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')'); - } - return json(['code' => 0, 'msg' => '修改解析记录成功!']); - } else { - return json(['code' => -1, 'msg' => '修改解析记录失败,' . $dns->getError()]); - } - } - - public function record_delete() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['code' => -1, 'msg' => '域名不存在']); - } - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - - $recordid = input('post.recordid', null, 'trim'); - $recordinfo = input('post.recordinfo', null, 'trim'); - - if (empty($recordid)) { - return json(['code' => -1, 'msg' => '参数不能为空']); - } - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - if ($dns->deleteDomainRecord($recordid)) { - if ($recordinfo) { - $recordinfo = json_decode($recordinfo, true); - if (is_array($recordinfo['Value'])) $recordinfo['Value'] = implode(',', $recordinfo['Value']); - $this->add_log($drow['name'], '删除解析', $recordinfo['Name'].' ['.$recordinfo['Type'].'] '.$recordinfo['Value'].' (线路:'.$recordinfo['Line'].' TTL:'.$recordinfo['TTL'].')'); - } else { - $this->add_log($drow['name'], '删除解析', '记录ID:'.$recordid); - } - return json(['code' => 0, 'msg' => '删除解析记录成功!']); - } else { - return json(['code' => -1, 'msg' => '删除解析记录失败,' . $dns->getError()]); - } - } - - public function record_status() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['code' => -1, 'msg' => '域名不存在']); - } - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - - $recordid = input('post.recordid', null, 'trim'); - $status = input('post.status', null, 'trim'); - $recordinfo = input('post.recordinfo', null, 'trim'); - - if (empty($recordid)) { - return json(['code' => -1, 'msg' => '参数不能为空']); - } - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - if ($dns->setDomainRecordStatus($recordid, $status)) { - $action = $status == '1' ? '启用解析' : '暂停解析'; - if ($recordinfo) { - $recordinfo = json_decode($recordinfo, true); - if (is_array($recordinfo['Value'])) $recordinfo['Value'] = implode(',', $recordinfo['Value']); - $this->add_log($drow['name'], $action, $recordinfo['Name'].' ['.$recordinfo['Type'].'] '.$recordinfo['Value'].' (线路:'.$recordinfo['Line'].' TTL:'.$recordinfo['TTL'].')'); - } else { - $this->add_log($drow['name'], $action, '记录ID:'.$recordid); - } - return json(['code' => 0, 'msg' => '操作成功!']); - } else { - return json(['code' => -1, 'msg' => '操作失败,' . $dns->getError()]); - } - } - - public function record_remark() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['code' => -1, 'msg' => '域名不存在']); - } - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - - $recordid = input('post.recordid', null, 'trim'); - $remark = input('post.remark', null, 'trim'); - - if (empty($recordid)) { - return json(['code' => -1, 'msg' => '参数不能为空']); - } - if (empty($remark)) $remark = null; - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - if ($dns->updateDomainRecordRemark($recordid, $remark)) { - return json(['code' => 0, 'msg' => '操作成功!']); - } else { - return json(['code' => -1, 'msg' => '操作失败,' . $dns->getError()]); - } - } - - public function record_batch() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['code' => -1, 'msg' => '域名不存在']); - } - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - - $action = input('post.action', null, 'trim'); - $recordinfo = input('post.recordinfo', null, 'trim'); - $recordinfo = json_decode($recordinfo, true); - - if (empty($recordinfo) || empty($action)) { - return json(['code' => -1, 'msg' => '参数不能为空']); - } - - $success = 0; - $fail = 0; - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - if ($action == 'open') { - foreach ($recordinfo as $record) { - if ($dns->setDomainRecordStatus($record['RecordId'], '1')) { - if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); - $this->add_log($drow['name'], '启用解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' (线路:'.$record['Line'].' TTL:'.$record['TTL'].')'); - $success++; - } - } - $msg = '成功启用' . $success . '条解析记录'; - } else if ($action == 'pause') { - foreach ($recordinfo as $record) { - if ($dns->setDomainRecordStatus($record['RecordId'], '0')) { - if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); - $this->add_log($drow['name'], '暂停解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' (线路:'.$record['Line'].' TTL:'.$record['TTL'].')'); - $success++; - } - } - $msg = '成功暂停' . $success . '条解析记录'; - } else if ($action == 'delete') { - foreach ($recordinfo as $record) { - if ($dns->deleteDomainRecord($record['RecordId'])) { - if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); - $this->add_log($drow['name'], '删除解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' (线路:'.$record['Line'].' TTL:'.$record['TTL'].')'); - $success++; - } - } - $msg = '成功删除' . $success . '条解析记录'; - } else if ($action == 'remark') { - $remark = input('post.remark', null, 'trim'); - if (empty($remark)) $remark = null; - foreach ($recordinfo as $record) { - if ($dns->updateDomainRecordRemark($record['RecordId'], $remark)) { - $success++; - } else { - $fail++; - } - } - $msg = '批量修改备注,成功' . $success . '条,失败' . $fail . '条'; - } - return json(['code' => 0, 'msg' => $msg]); - } - - public function record_batch_edit() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['code' => -1, 'msg' => '域名不存在']); - } - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - - $action = input('post.action', null, 'trim'); - $recordinfo = input('post.recordinfo', null, 'trim'); - $recordinfo = json_decode($recordinfo, true); - - if ($action == 'value') { - $type = input('post.type', null, 'trim'); - $value = input('post.value', null, 'trim'); - - if (empty($recordinfo) || empty($type) || empty($value)) { - return json(['code' => -1, 'msg' => '参数不能为空']); - } - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - - $success = 0; - $fail = 0; - foreach ($recordinfo as $record) { - $recordid = $dns->updateDomainRecord($record['RecordId'], $record['Name'], $type, $value, $record['Line'], $record['TTL'], $record['MX'], $record['Weight'], $record['Remark']); - if ($recordid) { - if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); - $this->add_log($drow['name'], '修改解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' → '.$record['Name'].' ['.$type.'] '.$value.' (线路:'.$record['Line'].' TTL:'.$record['TTL'].')'); - $success++; - } else { - $fail++; - } - } - return json(['code' => 0, 'msg' => '批量修改解析记录,成功' . $success . '条,失败' . $fail . '条']); - } else if ($action == 'line') { - $line = input('post.line', null, 'trim'); - - if (empty($recordinfo) || isNullOrEmpty($line)) { - return json(['code' => -1, 'msg' => '参数不能为空']); - } - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - - $success = 0; - $fail = 0; - foreach ($recordinfo as $record) { - $recordid = $dns->updateDomainRecord($record['RecordId'], $record['Name'], $record['Type'], $record['Value'], $line, $record['TTL'], $record['MX'], $record['Weight'], $record['Remark']); - if ($recordid) { - if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); - $this->add_log($drow['name'], '修改解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' (线路:'.$line.' TTL:'.$record['TTL'].')'); - $success++; - } else { - $fail++; - } - } - return json(['code' => 0, 'msg' => '批量修改解析线路,成功' . $success . '条,失败' . $fail . '条']); - } - } - - public function record_batch_add() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return $this->alert('error', '域名不存在'); - } - $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - - if (request()->isAjax()) { - $record = input('post.record', null, 'trim'); - $type = input('post.type', null, 'trim'); - $line = input('post.line', null, 'trim'); - $ttl = input('post.ttl/d', 600); - $mx = input('post.mx/d', 1); - $recordlist = explode("\n", $record); - - if (empty($record) || empty($recordlist)) { - return json(['code' => -1, 'msg' => '参数不能为空']); - } - if (is_null($line)) { - $line = DnsHelper::$line_name[$dnstype]['DEF']; - if ($dnstype == 'cloudflare' && input('post.proxy/d', 0) == 1) { - $line = '1'; - } - } - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - - $success = 0; - $fail = 0; - foreach ($recordlist as $record) { - $record = trim($record); - $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); - if ($recordid) { - $this->add_log($drow['name'], '添加解析', $arr[0].' ['.$thistype.'] '.$arr[1].' (线路:'.$line.' TTL:'.$ttl.')'); - $success++; - } else { - $fail++; - } - } - if ($success > 0) { - return json(['code' => 0, 'msg' => '批量添加解析,成功' . $success . '条,失败' . $fail . '条']); - } elseif($fail > 0) { - return json(['code' => -1, 'msg' => '批量添加解析失败,' . $dns->getError()]); - } else { - return json(['code' => -1, 'msg' => '批量添加解析失败,没有可添加的记录']); - } - } - - list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); - - $recordLineArr = []; - foreach ($recordLine as $key => $item) { - $recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']]; - } - - $dnsconfig = DnsHelper::$dns_config[$dnstype]; - $dnsconfig['type'] = $dnstype; - - View::assign('domainId', $id); - View::assign('domainName', $drow['name']); - View::assign('recordLine', $recordLineArr); - View::assign('minTTL', $minTTL ? $minTTL : 1); - View::assign('dnsconfig', $dnsconfig); - return view('batchadd'); - } - - public function record_batch_add2() - { - return view('batchadd2'); - } - - public function record_batch_edit2() - { - if (request()->isAjax()) { - $id = input('post.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['code' => -1, 'msg' => '域名不存在']); - } - $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); - if (!checkPermission(0, $drow['name'])) return json(['code' => -1, 'msg' => '无权限']); - - $name = input('post.name', null, 'trim'); - $type = input('post.type', null, 'trim'); - $value = input('post.value', null, 'trim'); - $ttl = input('post.ttl/d', 0); - $mx = input('post.mx/d', 0); - - if (empty($name) || empty($type) || empty($value)) { - return json(['code' => -1, 'msg' => '必填参数不能为空']); - } - $line = DnsHelper::$line_name[$dnstype]['DEF']; - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - $domainRecords = $dns->getSubDomainRecords($name, 1, 100); - if (!$domainRecords) return json(['code' => -1, 'msg' => '获取记录列表失败,' . $dns->getError()]); - if (empty($domainRecords['list'])) return json(['code' => -1, 'msg' => '没有可修改的记录']); - - if ($type == 'A' || $type == 'AAAA' || $type == 'CNAME') { - $list2 = array_filter($domainRecords['list'], function ($item) use ($type) { - return $item['Type'] == $type; - }); - if (!empty($list2)) { - $list = $list2; - } else { - $list = array_filter($domainRecords['list'], function ($item) { - return $item['Type'] == 'A' || $item['Type'] == 'AAAA' || $item['Type'] == 'CNAME'; - }); - } - } else { - $list = array_filter($domainRecords['list'], function ($item) use ($type) { - return $item['Type'] == $type; - }); - } - if (empty($list)) return json(['code' => -1, 'msg' => '没有可修改的'.$type.'记录']); - - $list2 = array_filter($domainRecords['list'], function ($item) use ($line) { - return $item['Line'] == $line; - }); - if (!empty($list2)) $list = $list2; - - $success = 0; - $fail = 0; - foreach ($list as $record) { - if ($name == '@' && ($record['Type'] == 'NS' || $record['Type'] == 'SOA')) continue; - - if ($ttl > 0) $record['TTL'] = $ttl; - if ($mx > 0) $record['MX'] = $mx; - $recordid = $dns->updateDomainRecord($record['RecordId'], $record['Name'], $type, $value, $record['Line'], $record['TTL'], $record['MX'], $record['Weight'], $record['Remark']); - if ($recordid) { - if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); - $this->add_log($drow['name'], '修改解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' → '.$record['Name'].' ['.$type.'] '.$value.' (线路:'.$record['Line'].' TTL:'.$record['TTL'].')'); - $success++; - } else { - $fail++; - } - } - if ($success > 0) { - return json(['code' => 0, 'msg' => '成功修改' . $success . '条解析记录']); - } elseif($fail > 0) { - return json(['code' => -1, 'msg' => $dns->getError()]); - } else { - return json(['code' => -1, 'msg' => '没有可修改的记录']); - } - } - - return view('batchedit'); - } - - public function record_log() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return $this->alert('error', '域名不存在'); - } - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - - if (request()->isPost()) { - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - $page = $offset / $limit + 1; - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - $domainRecords = $dns->getDomainRecordLog($page, $limit); - if (!$domainRecords) return json(['total' => 0, 'rows' => []]); - return json(['total' => $domainRecords['total'], 'rows' => $domainRecords['list']]); - } - - View::assign('domainId', $id); - View::assign('domainName', $drow['name']); - return view('log'); - } - - private function add_log($domain, $action, $data) - { - if (strlen($data) > 500) $data = substr($data, 0, 500); - Db::name('log')->insert(['uid' => request()->user['id'], 'domain' => $domain, 'action' => $action, 'data' => $data, 'addtime' => date("Y-m-d H:i:s")]); - } - - - public function weight() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return $this->alert('error', '域名不存在'); - } - if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); - if (request()->isAjax()) { - $act = input('param.act'); - if ($act == 'status') { - $subdomain = input('post.subdomain', null, 'trim'); - $status = input('post.status', null, 'trim'); - $type = input('post.type', null, 'trim'); - $line = input('post.line', null, 'trim'); - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - if ($dns->setWeightStatus($subdomain, $status, $type, $line)) { - return json(['code' => 0, 'msg' => '操作成功']); - } else { - return json(['code' => -1, 'msg' => '操作失败,' . $dns->getError()]); - } - } elseif ($act == 'update') { - $subdomain = input('post.subdomain', null, 'trim'); - $status = input('post.status', '0', 'trim'); - $type = input('post.type', null, 'trim'); - $line = input('post.line', null, 'trim'); - $weight = input('post.weight'); - if (empty($subdomain) || empty($type) || empty($line) || $status == '1' && empty($weight)) { - return json(['code' => -1, 'msg' => '参数不能为空']); - } - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - if ($type == 'CNAME' || $dns->setWeightStatus($subdomain, $status, $type, $line)) { - if ($status == '1') { - $success = 0; - foreach($weight as $recordid => $weight) { - if ($dns->updateRecordWeight($recordid, $weight)) { - $success++; - } - } - if ($success > 0) { - return json(['code' => 0, 'msg' => '成功修改' . $success . '条解析记录权重']); - } else { - return json(['code' => -1, 'msg' => '修改权重失败,' . $dns->getError()]); - } - } - return json(['code' => 0, 'msg' => '修改成功']); - } else { - return json(['code' => -1, 'msg' => '修改失败,' . $dns->getError()]); - } - } else { - return json(['code' => -1, 'msg' => '参数错误']); - } - } - - $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); - if ($dnstype != 'aliyun') { - return $this->alert('error', '仅支持阿里云解析的域名'); - } - list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); - - $recordLineArr = []; - foreach ($recordLine as $key => $item) { - $recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']]; - } - - $dnsconfig = DnsHelper::$dns_config[$dnstype]; - $dnsconfig['type'] = $dnstype; - - View::assign('domainId', $id); - View::assign('domainName', $drow['name']); - View::assign('recordLine', $recordLineArr); - View::assign('dnsconfig', $dnsconfig); - return view(); - } - - public function weight_data() - { - $id = input('param.id/d'); - $keyword = input('post.keyword', null, 'trim'); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - if ($limit == 0) { - $page = 1; - } else { - $page = $offset / $limit + 1; - } - - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['total' => 0, 'rows' => []]); - } - if (!checkPermission(0, $drow['name'])) return json(['total' => 0, 'rows' => []]); - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - $domainRecords = $dns->getWeightSubDomains($page, $limit, $keyword); - return json(['total' => $domainRecords['total'], 'rows' => $domainRecords['list']]); - } - - public function expire_notice() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - if ($this->request->isPost()) { - $params = input('post.'); - foreach ($params as $key => $value) { - if (empty($key)) { - continue; - } - config_set($key, $value); - Cache::delete('configs'); - } - return json(['code' => 0, 'msg' => 'succ']); - } - return View::fetch(); - } - - public function update_date() - { - $id = input('param.id/d'); - $drow = Db::name('domain')->where('id', $id)->find(); - if (!$drow) { - return json(['code' => -1, 'msg' => '域名不存在']); - } - if (!checkPermission(0, $drow['name'])) return json(['code' => -1, 'msg' => '无权限']); - $result = (new ExpireNoticeService())->updateDomainDate($id, $drow['name']); - return json($result); - } -} +alert('error', '无权限'); + View::assign('dnsconfig', DnsHelper::$dns_config); + return view(); + } + + public function account_data() + { + if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); + $kw = $this->request->post('kw', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('account'); + if (!empty($kw)) { + $select->whereLike('ak|remark', '%' . $kw . '%'); + } + $total = $select->count(); + $rows = $select->order('id', 'desc')->limit($offset, $limit)->select(); + + $list = []; + foreach ($rows as $row) { + $row['typename'] = DnsHelper::$dns_config[$row['type']]['name']; + $list[] = $row; + } + + return json(['total' => $total, 'rows' => $list]); + } + + 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') { + $type = input('post.type'); + $ak = input('post.ak', null, 'trim'); + $sk = input('post.sk', null, 'trim'); + $ext = input('post.ext', 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()) { + return json(['code' => -1, 'msg' => '域名账户已存在']); + } + Db::startTrans(); + $id = Db::name('account')->insertGetId([ + 'type' => $type, + 'ak' => $ak, + 'sk' => $sk, + 'ext' => $ext, + 'proxy' => $proxy, + 'remark' => $remark, + 'addtime' => date('Y-m-d H:i:s'), + ]); + $dns = DnsHelper::getModel($id); + if ($dns) { + if ($dns->check()) { + Db::commit(); + return json(['code' => 0, 'msg' => '添加域名账户成功!']); + } else { + Db::rollback(); + return json(['code' => -1, 'msg' => '验证域名账户失败,' . $dns->getError()]); + } + } else { + Db::rollback(); + return json(['code' => -1, 'msg' => 'DNS模块(' . $type . ')不存在']); + } + } elseif ($act == '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'); + $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()) { + return json(['code' => -1, 'msg' => '域名账户已存在']); + } + Db::startTrans(); + Db::name('account')->where('id', $id)->update([ + 'type' => $type, + 'ak' => $ak, + 'sk' => $sk, + 'ext' => $ext, + 'proxy' => $proxy, + 'remark' => $remark, + ]); + $dns = DnsHelper::getModel($id); + if ($dns) { + if ($dns->check()) { + Db::commit(); + return json(['code' => 0, 'msg' => '修改域名账户成功!']); + } else { + Db::rollback(); + return json(['code' => -1, 'msg' => '验证域名账户失败,' . $dns->getError()]); + } + } else { + Db::rollback(); + return json(['code' => -1, 'msg' => 'DNS模块(' . $type . ')不存在']); + } + } elseif ($act == 'del') { + $id = input('post.id/d'); + $dcount = DB::name('domain')->where('aid', $id)->count(); + if ($dcount > 0) return json(['code' => -1, 'msg' => '该域名账户下存在域名,无法删除']); + Db::name('account')->where('id', $id)->delete(); + return json(['code' => 0]); + } + return json(['code' => -3]); + } + + + public function domain() + { + if (request()->user['type'] == 'domain') { + return redirect('/record/' . request()->user['id']); + } + $list = Db::name('account')->select(); + $accounts = []; + $types = []; + foreach ($list as $row) { + $name = $row['id'] . '_' . DnsHelper::$dns_config[$row['type']]['name']; + if (!array_key_exists($row['type'], $types)) { + $types[$row['type']] = DnsHelper::$dns_config[$row['type']]['name']; + } + if (!empty($row['remark'])) { + $name .= '(' . $row['remark'] . ')'; + } + $accounts[] = ['id' => $row['id'], 'name' => $name, 'type' => DnsHelper::$dns_config[$row['type']]['name'], 'add' => DnsHelper::$dns_config[$row['type']]['add']]; + } + View::assign('accounts', $accounts); + View::assign('types', $types); + return view(); + } + + public function domain_add() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $list = Db::name('account')->select(); + $accounts = []; + $types = []; + foreach ($list as $row) { + $accounts[$row['id']] = $row['id'] . '_' . DnsHelper::$dns_config[$row['type']]['name']; + if (!array_key_exists($row['type'], $types)) { + $types[$row['type']] = DnsHelper::$dns_config[$row['type']]['name']; + } + if (!empty($row['remark'])) { + $accounts[$row['id']] .= '(' . $row['remark'] . ')'; + } + } + View::assign('accounts', $accounts); + View::assign('types', $types); + return view(); + } + + public function domain_data() + { + if (!checkPermission(1)) return json(['total' => 0, 'rows' => []]); + $kw = input('post.kw', null, 'trim'); + $type = input('post.type', null, 'trim'); + $status = input('post.status', null, 'trim'); + $offset = input('post.offset/d', 0); + $limit = input('post.limit/d', 10); + + $select = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id'); + if (!empty($kw)) { + $select->whereLike('name|A.remark', '%' . $kw . '%'); + } + if (!empty($type)) { + $select->whereLike('B.type', $type); + } + if (request()->user['level'] == 1) { + $select->where('is_hide', 0)->where('A.name', 'in', request()->user['permission']); + } + if (!isNullOrEmpty($status)) { + if ($status == '2') { + $select->where('A.expiretime', '<=', date('Y-m-d H:i:s')); + } elseif ($status == '1') { + $select->where('A.expiretime', '<=', date('Y-m-d H:i:s', time() + 86400 * 30))->where('A.expiretime', '>', date('Y-m-d H:i:s')); + } + } + $total = $select->count(); + $rows = $select->fieldRaw('A.*,B.type,B.remark aremark')->order('A.id', 'desc')->limit($offset, $limit)->select(); + + $list = []; + foreach ($rows as $row) { + $row['typename'] = DnsHelper::$dns_config[$row['type']]['name']; + $list[] = $row; + } + + return json(['total' => $total, 'rows' => $list]); + } + + public function domain_op() + { + if (!checkPermission(1)) return $this->alert('error', '无权限'); + $act = input('param.act'); + if ($act == 'get') { + $id = input('post.id/d'); + $row = Db::name('domain')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => '域名不存在']); + return json(['code' => 0, 'data' => $row]); + } elseif ($act == 'add') { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $aid = input('post.aid/d'); + $method = input('post.method/d', 0); + $name = input('post.name', null, 'trim'); + $thirdid = input('post.thirdid', null, 'trim'); + $recordcount = input('post.recordcount/d', 0); + if ($method == 1 && empty($name) || $method == 0 && (empty($name) || empty($thirdid))) return json(['code' => -1, 'msg' => '参数不能为空']); + if (Db::name('domain')->where('aid', $aid)->where('name', $name)->find()) { + return json(['code' => -1, 'msg' => '域名已存在']); + } + if ($method == 1) { + $dns = DnsHelper::getModel($aid); + $result = $dns->addDomain($name); + if (!$result) return json(['code' => -1, 'msg' => '添加域名失败,' . $dns->getError()]); + $name = $result['name']; + $thirdid = $result['id']; + } + Db::name('domain')->insert([ + 'aid' => $aid, + 'name' => $name, + 'thirdid' => $thirdid, + 'addtime' => date('Y-m-d H:i:s'), + 'is_hide' => 0, + 'is_sso' => 1, + 'recordcount' => $recordcount, + ]); + return json(['code' => 0, 'msg' => '添加域名成功!']); + } elseif ($act == 'edit') { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $id = input('post.id/d'); + $row = Db::name('domain')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => '域名不存在']); + $is_hide = input('post.is_hide/d'); + $is_sso = input('post.is_sso/d'); + $is_notice = input('post.is_notice/d'); + $expiretime = input('post.expiretime', null, 'trim'); + $remark = input('post.remark', null, 'trim'); + if (empty($remark)) $remark = null; + Db::name('domain')->where('id', $id)->update([ + 'is_hide' => $is_hide, + 'is_sso' => $is_sso, + 'is_notice' => $is_notice, + 'expiretime' => $expiretime ? $expiretime : null, + 'remark' => $remark, + ]); + return json(['code' => 0, 'msg' => '修改域名配置成功!']); + } elseif ($act == 'del') { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $id = input('post.id/d'); + Db::name('domain')->where('id', $id)->delete(); + Db::name('dmtask')->where('did', $id)->delete(); + Db::name('optimizeip')->where('did', $id)->delete(); + Db::name('sctask')->where('did', $id)->delete(); + return json(['code' => 0]); + } elseif ($act == 'batchadd') { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $aid = input('post.aid/d'); + $domains = input('post.domains'); + if (empty($domains)) return json(['code' => -1, 'msg' => '参数不能为空']); + $data = []; + foreach ($domains as $row) { + $data[] = [ + 'aid' => $aid, + 'name' => $row['name'], + 'thirdid' => $row['id'], + 'addtime' => date('Y-m-d H:i:s'), + 'is_hide' => 0, + 'is_sso' => 1, + 'recordcount' => $row['recordcount'], + ]; + } + Db::name('domain')->insertAll($data); + return json(['code' => 0, 'msg' => '成功添加' . count($data) . '个域名!']); + } elseif ($act == 'batchedit') { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $ids = input('post.ids'); + if (empty($ids)) return json(['code' => -1, 'msg' => '参数不能为空']); + $remark = input('post.remark', null, 'trim'); + if (empty($remark)) $remark = null; + $count = Db::name('domain')->where('id', 'in', $ids)->update(['remark' => $remark]); + return json(['code' => 0, 'msg' => '成功修改' . $count . '个域名!']); + } elseif ($act == 'batchsetnotice') { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $ids = input('post.ids'); + $is_notice = input('post.is_notice/d', 0); + if (empty($ids)) return json(['code' => -1, 'msg' => '参数不能为空']); + $count = Db::name('domain')->where('id', 'in', $ids)->update(['is_notice' => $is_notice]); + return json(['code' => 0, 'msg' => '成功修改' . $count . '个域名!']); + } elseif ($act == 'batchdel') { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $ids = input('post.ids'); + if (empty($ids)) return json(['code' => -1, 'msg' => '参数不能为空']); + Db::name('domain')->where('id', 'in', $ids)->delete(); + Db::name('dmtask')->where('did', 'in', $ids)->delete(); + Db::name('optimizeip')->where('did', 'in', $ids)->delete(); + Db::name('sctask')->where('did', 'in', $ids)->delete(); + return json(['code' => 0, 'msg' => '成功删除' . count($ids) . '个域名!']); + } + return json(['code' => -3]); + } + + public function domain_list() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $aid = input('post.aid/d'); + $kw = input('post.kw', null, 'trim'); + $page = input('?post.page') ? input('post.page/d') : 1; + $pagesize = input('?post.pagesize') ? input('post.pagesize/d') : 10; + $dns = DnsHelper::getModel($aid); + $result = $dns->getDomainList($kw, $page, $pagesize); + if (!$result) return json(['code' => -1, 'msg' => '获取域名列表失败,' . $dns->getError()]); + + foreach ($result['list'] as &$row) { + $row['disabled'] = Db::name('domain')->where('aid', $aid)->where('name', $row['Domain'])->find() != null; + } + return json(['code' => 0, 'data' => ['total' => $result['total'], 'list' => $result['list']]]); + } + + //获取解析线路和最小TTL + private function get_line_and_ttl($drow) + { + $recordLine = cache('record_line_' . $drow['id']); + $minTTL = cache('min_ttl_' . $drow['id']); + if (empty($recordLine)) { + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + if (!$dns) throw new Exception('DNS模块不存在'); + $recordLine = $dns->getRecordLine(); + if (!$recordLine) throw new Exception('获取解析线路列表失败,' . $dns->getError()); + cache('record_line_' . $drow['id'], $recordLine, 604800); + $minTTL = $dns->getMinTTL(); + if ($minTTL) { + cache('min_ttl_' . $drow['id'], $minTTL, 604800); + } + } + return [$recordLine, $minTTL]; + } + + public function domain_info() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return $this->alert('error', '域名不存在'); + } + $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); + + $recordLineArr = []; + foreach ($recordLine as $key => $item) { + $recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']]; + } + + $dnsconfig = DnsHelper::$dns_config[$dnstype]; + $dnsconfig['type'] = $dnstype; + + $drow['config'] = $dnsconfig; + $drow['recordLine'] = $recordLineArr; + $drow['minTTL'] = $minTTL ? $minTTL : 1; + if (input('?post.loginurl') && input('post.loginurl') == '1') { + $token = getSid(); + cache('quicklogin_' . $drow['name'], $token, 3600); + $timestamp = time(); + $sign = md5(config_get('sys_key') . $drow['name'] . $timestamp . $token . config_get('sys_key')); + $drow['loginurl'] = request()->root(true) . '/quicklogin?domain=' . $drow['name'] . '×tamp=' . $timestamp . '&token=' . $token . '&sign=' . $sign; + } + + return json(['code' => 0, 'data' => $drow]); + } + + public function record() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return $this->alert('error', '域名不存在'); + } + $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); + + $recordLineArr = []; + foreach ($recordLine as $key => $item) { + $recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']]; + } + + $dnsconfig = DnsHelper::$dns_config[$dnstype]; + $dnsconfig['type'] = $dnstype; + + View::assign('domainId', $id); + View::assign('domainName', $drow['name']); + View::assign('recordLine', $recordLineArr); + View::assign('minTTL', $minTTL ? $minTTL : 1); + View::assign('dnsconfig', $dnsconfig); + return view(); + } + + public function record_data() + { + $id = input('param.id/d'); + $keyword = input('post.keyword', null, 'trim'); + $subdomain = input('post.subdomain', null, 'trim'); + $value = input('post.value', null, 'trim'); + $type = input('post.type', null, 'trim'); + $line = input('post.line', null, 'trim'); + $status = input('post.status', null, 'trim'); + $offset = input('post.offset/d', 0); + $limit = input('post.limit/d', 10); + if ($limit == 0) { + $page = 1; + } else { + $page = $offset / $limit + 1; + } + + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['total' => 0, 'rows' => []]); + } + if (!checkPermission(0, $drow['name'])) return json(['total' => 0, 'rows' => []]); + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + $domainRecords = $dns->getDomainRecords($page, $limit, $keyword, $subdomain, $value, $type, $line, $status); + if (!$domainRecords) return json(['total' => 0, 'rows' => []]); + + if (empty($keyword) && empty($subdomain) && empty($type) && isNullOrEmpty($line) && empty($status) && empty($value) && $domainRecords['total'] != $drow['recordcount']) { + Db::name('domain')->where('id', $id)->update(['recordcount' => $domainRecords['total']]); + } + + $recordLine = cache('record_line_' . $id); + + foreach ($domainRecords['list'] as &$row) { + $row['LineName'] = isset($recordLine[$row['Line']]) ? $recordLine[$row['Line']]['name'] : $row['Line']; + } + + $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); + if (DnsHelper::$dns_config[$dnstype]['page']) { + return json($domainRecords['list']); + } + + return json(['total' => $domainRecords['total'], 'rows' => $domainRecords['list']]); + } + + public function record_list() + { + $id = input('post.id/d'); + $rr = input('post.rr', null, 'trim'); + + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['code' => -1, 'msg' => '域名不存在']); + } + if (!checkPermission(0, $drow['name'])) return json(['code' => -1, 'msg' => '无权限']); + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + $domainRecords = $dns->getSubDomainRecords($rr, 1, 100); + if (!$domainRecords) return json(['code' => -1, 'msg' => '获取记录列表失败,' . $dns->getError()]); + + list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); + + $list = []; + foreach ($domainRecords['list'] as &$row) { + if ($rr == '@' && ($row['Type'] == 'NS' || $row['Type'] == 'SOA')) continue; + $row['LineName'] = isset($recordLine[$row['Line']]) ? $recordLine[$row['Line']]['name'] : $row['Line']; + $list[] = $row; + } + + return json(['code' => 0, 'data' => $list]); + } + + public function record_add() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['code' => -1, 'msg' => '域名不存在']); + } + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + $name = input('post.name', null, 'trim'); + $type = input('post.type', null, 'trim'); + $value = input('post.value', null, 'trim'); + $line = input('post.line', null, 'trim'); + $ttl = input('post.ttl/d', 600); + $weight = input('post.weight/d', 0); + $mx = input('post.mx/d', 1); + $remark = input('post.remark', null, 'trim'); + + if (empty($name) || empty($type) || empty($value)) { + return json(['code' => -1, 'msg' => '参数不能为空']); + } + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + $recordid = $dns->addDomainRecord($name, $type, $value, $line, $ttl, $mx, $weight, $remark); + if ($recordid) { + $this->add_log($drow['name'], '添加解析', $name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')'); + return json(['code' => 0, 'msg' => '添加解析记录成功!']); + } else { + return json(['code' => -1, 'msg' => '添加解析记录失败,' . $dns->getError()]); + } + } + + public function record_update() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['code' => -1, 'msg' => '域名不存在']); + } + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + $recordid = input('post.recordid', null, 'trim'); + $name = input('post.name', null, 'trim'); + $type = input('post.type', null, 'trim'); + $value = input('post.value', null, 'trim'); + $line = input('post.line', null, 'trim'); + $ttl = input('post.ttl/d', 600); + $weight = input('post.weight/d', 0); + $mx = input('post.mx/d', 1); + $remark = input('post.remark', null, 'trim'); + + $recordinfo = input('post.recordinfo', null, 'trim'); + + if (empty($recordid) || empty($name) || empty($type) || empty($value)) { + return json(['code' => -1, 'msg' => '参数不能为空']); + } + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + $recordid = $dns->updateDomainRecord($recordid, $name, $type, $value, $line, $ttl, $mx, $weight, $remark); + if ($recordid) { + if ($recordinfo) { + $recordinfo = json_decode($recordinfo, true); + if (is_array($recordinfo['Value'])) $recordinfo['Value'] = implode(',', $recordinfo['Value']); + if ($recordinfo['Name'] != $name || $recordinfo['Type'] != $type || $recordinfo['Value'] != $value) { + $this->add_log($drow['name'], '修改解析', $recordinfo['Name'].' ['.$recordinfo['Type'].'] '.$recordinfo['Value'].' → '.$name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')'); + } elseif($recordinfo['Line'] != $line || $recordinfo['TTL'] != $ttl) { + $this->add_log($drow['name'], '修改解析', $name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')'); + } + } else { + $this->add_log($drow['name'], '修改解析', $name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')'); + } + return json(['code' => 0, 'msg' => '修改解析记录成功!']); + } else { + return json(['code' => -1, 'msg' => '修改解析记录失败,' . $dns->getError()]); + } + } + + public function record_delete() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['code' => -1, 'msg' => '域名不存在']); + } + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + $recordid = input('post.recordid', null, 'trim'); + $recordinfo = input('post.recordinfo', null, 'trim'); + + if (empty($recordid)) { + return json(['code' => -1, 'msg' => '参数不能为空']); + } + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + if ($dns->deleteDomainRecord($recordid)) { + if ($recordinfo) { + $recordinfo = json_decode($recordinfo, true); + if (is_array($recordinfo['Value'])) $recordinfo['Value'] = implode(',', $recordinfo['Value']); + $this->add_log($drow['name'], '删除解析', $recordinfo['Name'].' ['.$recordinfo['Type'].'] '.$recordinfo['Value'].' (线路:'.$recordinfo['Line'].' TTL:'.$recordinfo['TTL'].')'); + } else { + $this->add_log($drow['name'], '删除解析', '记录ID:'.$recordid); + } + return json(['code' => 0, 'msg' => '删除解析记录成功!']); + } else { + return json(['code' => -1, 'msg' => '删除解析记录失败,' . $dns->getError()]); + } + } + + public function record_status() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['code' => -1, 'msg' => '域名不存在']); + } + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + $recordid = input('post.recordid', null, 'trim'); + $status = input('post.status', null, 'trim'); + $recordinfo = input('post.recordinfo', null, 'trim'); + + if (empty($recordid)) { + return json(['code' => -1, 'msg' => '参数不能为空']); + } + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + if ($dns->setDomainRecordStatus($recordid, $status)) { + $action = $status == '1' ? '启用解析' : '暂停解析'; + if ($recordinfo) { + $recordinfo = json_decode($recordinfo, true); + if (is_array($recordinfo['Value'])) $recordinfo['Value'] = implode(',', $recordinfo['Value']); + $this->add_log($drow['name'], $action, $recordinfo['Name'].' ['.$recordinfo['Type'].'] '.$recordinfo['Value'].' (线路:'.$recordinfo['Line'].' TTL:'.$recordinfo['TTL'].')'); + } else { + $this->add_log($drow['name'], $action, '记录ID:'.$recordid); + } + return json(['code' => 0, 'msg' => '操作成功!']); + } else { + return json(['code' => -1, 'msg' => '操作失败,' . $dns->getError()]); + } + } + + public function record_remark() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['code' => -1, 'msg' => '域名不存在']); + } + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + $recordid = input('post.recordid', null, 'trim'); + $remark = input('post.remark', null, 'trim'); + + if (empty($recordid)) { + return json(['code' => -1, 'msg' => '参数不能为空']); + } + if (empty($remark)) $remark = null; + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + if ($dns->updateDomainRecordRemark($recordid, $remark)) { + return json(['code' => 0, 'msg' => '操作成功!']); + } else { + return json(['code' => -1, 'msg' => '操作失败,' . $dns->getError()]); + } + } + + public function record_batch() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['code' => -1, 'msg' => '域名不存在']); + } + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + $action = input('post.action', null, 'trim'); + $recordinfo = input('post.recordinfo', null, 'trim'); + $recordinfo = json_decode($recordinfo, true); + + if (empty($recordinfo) || empty($action)) { + return json(['code' => -1, 'msg' => '参数不能为空']); + } + + $success = 0; + $fail = 0; + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + if ($action == 'open') { + foreach ($recordinfo as $record) { + if ($dns->setDomainRecordStatus($record['RecordId'], '1')) { + if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); + $this->add_log($drow['name'], '启用解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' (线路:'.$record['Line'].' TTL:'.$record['TTL'].')'); + $success++; + } + } + $msg = '成功启用' . $success . '条解析记录'; + } else if ($action == 'pause') { + foreach ($recordinfo as $record) { + if ($dns->setDomainRecordStatus($record['RecordId'], '0')) { + if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); + $this->add_log($drow['name'], '暂停解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' (线路:'.$record['Line'].' TTL:'.$record['TTL'].')'); + $success++; + } + } + $msg = '成功暂停' . $success . '条解析记录'; + } else if ($action == 'delete') { + foreach ($recordinfo as $record) { + if ($dns->deleteDomainRecord($record['RecordId'])) { + if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); + $this->add_log($drow['name'], '删除解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' (线路:'.$record['Line'].' TTL:'.$record['TTL'].')'); + $success++; + } + } + $msg = '成功删除' . $success . '条解析记录'; + } else if ($action == 'remark') { + $remark = input('post.remark', null, 'trim'); + if (empty($remark)) $remark = null; + foreach ($recordinfo as $record) { + if ($dns->updateDomainRecordRemark($record['RecordId'], $remark)) { + $success++; + } else { + $fail++; + } + } + $msg = '批量修改备注,成功' . $success . '条,失败' . $fail . '条'; + } + return json(['code' => 0, 'msg' => $msg]); + } + + public function record_batch_edit() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['code' => -1, 'msg' => '域名不存在']); + } + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + $action = input('post.action', null, 'trim'); + $recordinfo = input('post.recordinfo', null, 'trim'); + $recordinfo = json_decode($recordinfo, true); + + if ($action == 'value') { + $type = input('post.type', null, 'trim'); + $value = input('post.value', null, 'trim'); + + if (empty($recordinfo) || empty($type) || empty($value)) { + return json(['code' => -1, 'msg' => '参数不能为空']); + } + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + + $success = 0; + $fail = 0; + foreach ($recordinfo as $record) { + $recordid = $dns->updateDomainRecord($record['RecordId'], $record['Name'], $type, $value, $record['Line'], $record['TTL'], $record['MX'], $record['Weight'], $record['Remark']); + if ($recordid) { + if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); + $this->add_log($drow['name'], '修改解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' → '.$record['Name'].' ['.$type.'] '.$value.' (线路:'.$record['Line'].' TTL:'.$record['TTL'].')'); + $success++; + } else { + $fail++; + } + } + return json(['code' => 0, 'msg' => '批量修改解析记录,成功' . $success . '条,失败' . $fail . '条']); + } else if ($action == 'line') { + $line = input('post.line', null, 'trim'); + + if (empty($recordinfo) || isNullOrEmpty($line)) { + return json(['code' => -1, 'msg' => '参数不能为空']); + } + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + + $success = 0; + $fail = 0; + foreach ($recordinfo as $record) { + $recordid = $dns->updateDomainRecord($record['RecordId'], $record['Name'], $record['Type'], $record['Value'], $line, $record['TTL'], $record['MX'], $record['Weight'], $record['Remark']); + if ($recordid) { + if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); + $this->add_log($drow['name'], '修改解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' (线路:'.$line.' TTL:'.$record['TTL'].')'); + $success++; + } else { + $fail++; + } + } + return json(['code' => 0, 'msg' => '批量修改解析线路,成功' . $success . '条,失败' . $fail . '条']); + } + } + + public function record_batch_add() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return $this->alert('error', '域名不存在'); + } + $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + if (request()->isAjax()) { + $record = input('post.record', null, 'trim'); + $type = input('post.type', null, 'trim'); + $line = input('post.line', null, 'trim'); + $ttl = input('post.ttl/d', 600); + $mx = input('post.mx/d', 1); + $recordlist = explode("\n", $record); + + if (empty($record) || empty($recordlist)) { + return json(['code' => -1, 'msg' => '参数不能为空']); + } + if (is_null($line)) { + $line = DnsHelper::$line_name[$dnstype]['DEF']; + if ($dnstype == 'cloudflare' && input('post.proxy/d', 0) == 1) { + $line = '1'; + } + } + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + + $success = 0; + $fail = 0; + foreach ($recordlist as $record) { + $record = trim($record); + $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); + if ($recordid) { + $this->add_log($drow['name'], '添加解析', $arr[0].' ['.$thistype.'] '.$arr[1].' (线路:'.$line.' TTL:'.$ttl.')'); + $success++; + } else { + $fail++; + } + } + if ($success > 0) { + return json(['code' => 0, 'msg' => '批量添加解析,成功' . $success . '条,失败' . $fail . '条']); + } elseif($fail > 0) { + return json(['code' => -1, 'msg' => '批量添加解析失败,' . $dns->getError()]); + } else { + return json(['code' => -1, 'msg' => '批量添加解析失败,没有可添加的记录']); + } + } + + list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); + + $recordLineArr = []; + foreach ($recordLine as $key => $item) { + $recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']]; + } + + $dnsconfig = DnsHelper::$dns_config[$dnstype]; + $dnsconfig['type'] = $dnstype; + + View::assign('domainId', $id); + View::assign('domainName', $drow['name']); + View::assign('recordLine', $recordLineArr); + View::assign('minTTL', $minTTL ? $minTTL : 1); + View::assign('dnsconfig', $dnsconfig); + return view('batchadd'); + } + + public function record_batch_add2() + { + return view('batchadd2'); + } + + public function record_batch_edit2() + { + if (request()->isAjax()) { + $id = input('post.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['code' => -1, 'msg' => '域名不存在']); + } + $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); + if (!checkPermission(0, $drow['name'])) return json(['code' => -1, 'msg' => '无权限']); + + $name = input('post.name', null, 'trim'); + $type = input('post.type', null, 'trim'); + $value = input('post.value', null, 'trim'); + $ttl = input('post.ttl/d', 0); + $mx = input('post.mx/d', 0); + + if (empty($name) || empty($type) || empty($value)) { + return json(['code' => -1, 'msg' => '必填参数不能为空']); + } + $line = DnsHelper::$line_name[$dnstype]['DEF']; + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + $domainRecords = $dns->getSubDomainRecords($name, 1, 100); + if (!$domainRecords) return json(['code' => -1, 'msg' => '获取记录列表失败,' . $dns->getError()]); + if (empty($domainRecords['list'])) return json(['code' => -1, 'msg' => '没有可修改的记录']); + + if ($type == 'A' || $type == 'AAAA' || $type == 'CNAME') { + $list2 = array_filter($domainRecords['list'], function ($item) use ($type) { + return $item['Type'] == $type; + }); + if (!empty($list2)) { + $list = $list2; + } else { + $list = array_filter($domainRecords['list'], function ($item) { + return $item['Type'] == 'A' || $item['Type'] == 'AAAA' || $item['Type'] == 'CNAME'; + }); + } + } else { + $list = array_filter($domainRecords['list'], function ($item) use ($type) { + return $item['Type'] == $type; + }); + } + if (empty($list)) return json(['code' => -1, 'msg' => '没有可修改的'.$type.'记录']); + + $list2 = array_filter($domainRecords['list'], function ($item) use ($line) { + return $item['Line'] == $line; + }); + if (!empty($list2)) $list = $list2; + + $success = 0; + $fail = 0; + foreach ($list as $record) { + if ($name == '@' && ($record['Type'] == 'NS' || $record['Type'] == 'SOA')) continue; + + if ($ttl > 0) $record['TTL'] = $ttl; + if ($mx > 0) $record['MX'] = $mx; + $recordid = $dns->updateDomainRecord($record['RecordId'], $record['Name'], $type, $value, $record['Line'], $record['TTL'], $record['MX'], $record['Weight'], $record['Remark']); + if ($recordid) { + if (is_array($record['Value'])) $record['Value'] = implode(',', $record['Value']); + $this->add_log($drow['name'], '修改解析', $record['Name'].' ['.$record['Type'].'] '.$record['Value'].' → '.$record['Name'].' ['.$type.'] '.$value.' (线路:'.$record['Line'].' TTL:'.$record['TTL'].')'); + $success++; + } else { + $fail++; + } + } + if ($success > 0) { + return json(['code' => 0, 'msg' => '成功修改' . $success . '条解析记录']); + } elseif($fail > 0) { + return json(['code' => -1, 'msg' => $dns->getError()]); + } else { + return json(['code' => -1, 'msg' => '没有可修改的记录']); + } + } + + return view('batchedit'); + } + + public function record_log() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return $this->alert('error', '域名不存在'); + } + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + + if (request()->isPost()) { + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + $page = $offset / $limit + 1; + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + $domainRecords = $dns->getDomainRecordLog($page, $limit); + if (!$domainRecords) return json(['total' => 0, 'rows' => []]); + return json(['total' => $domainRecords['total'], 'rows' => $domainRecords['list']]); + } + + View::assign('domainId', $id); + View::assign('domainName', $drow['name']); + return view('log'); + } + + private function add_log($domain, $action, $data) + { + if (strlen($data) > 500) $data = substr($data, 0, 500); + Db::name('log')->insert(['uid' => request()->user['id'], 'domain' => $domain, 'action' => $action, 'data' => $data, 'addtime' => date("Y-m-d H:i:s")]); + } + + + public function weight() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return $this->alert('error', '域名不存在'); + } + if (!checkPermission(0, $drow['name'])) return $this->alert('error', '无权限'); + if (request()->isAjax()) { + $act = input('param.act'); + if ($act == 'status') { + $subdomain = input('post.subdomain', null, 'trim'); + $status = input('post.status', null, 'trim'); + $type = input('post.type', null, 'trim'); + $line = input('post.line', null, 'trim'); + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + if ($dns->setWeightStatus($subdomain, $status, $type, $line)) { + return json(['code' => 0, 'msg' => '操作成功']); + } else { + return json(['code' => -1, 'msg' => '操作失败,' . $dns->getError()]); + } + } elseif ($act == 'update') { + $subdomain = input('post.subdomain', null, 'trim'); + $status = input('post.status', '0', 'trim'); + $type = input('post.type', null, 'trim'); + $line = input('post.line', null, 'trim'); + $weight = input('post.weight'); + if (empty($subdomain) || empty($type) || empty($line) || $status == '1' && empty($weight)) { + return json(['code' => -1, 'msg' => '参数不能为空']); + } + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + if ($type == 'CNAME' || $dns->setWeightStatus($subdomain, $status, $type, $line)) { + if ($status == '1') { + $success = 0; + foreach($weight as $recordid => $weight) { + if ($dns->updateRecordWeight($recordid, $weight)) { + $success++; + } + } + if ($success > 0) { + return json(['code' => 0, 'msg' => '成功修改' . $success . '条解析记录权重']); + } else { + return json(['code' => -1, 'msg' => '修改权重失败,' . $dns->getError()]); + } + } + return json(['code' => 0, 'msg' => '修改成功']); + } else { + return json(['code' => -1, 'msg' => '修改失败,' . $dns->getError()]); + } + } else { + return json(['code' => -1, 'msg' => '参数错误']); + } + } + + $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); + if ($dnstype != 'aliyun') { + return $this->alert('error', '仅支持阿里云解析的域名'); + } + list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); + + $recordLineArr = []; + foreach ($recordLine as $key => $item) { + $recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']]; + } + + $dnsconfig = DnsHelper::$dns_config[$dnstype]; + $dnsconfig['type'] = $dnstype; + + View::assign('domainId', $id); + View::assign('domainName', $drow['name']); + View::assign('recordLine', $recordLineArr); + View::assign('dnsconfig', $dnsconfig); + return view(); + } + + public function weight_data() + { + $id = input('param.id/d'); + $keyword = input('post.keyword', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + if ($limit == 0) { + $page = 1; + } else { + $page = $offset / $limit + 1; + } + + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['total' => 0, 'rows' => []]); + } + if (!checkPermission(0, $drow['name'])) return json(['total' => 0, 'rows' => []]); + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + $domainRecords = $dns->getWeightSubDomains($page, $limit, $keyword); + return json(['total' => $domainRecords['total'], 'rows' => $domainRecords['list']]); + } + + public function expire_notice() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + if ($this->request->isPost()) { + $params = input('post.'); + foreach ($params as $key => $value) { + if (empty($key)) { + continue; + } + config_set($key, $value); + Cache::delete('configs'); + } + return json(['code' => 0, 'msg' => 'succ']); + } + return View::fetch(); + } + + public function update_date() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['code' => -1, 'msg' => '域名不存在']); + } + if (!checkPermission(0, $drow['name'])) return json(['code' => -1, 'msg' => '无权限']); + $result = (new ExpireNoticeService())->updateDomainDate($id, $drow['name']); + return json($result); + } +} diff --git a/app/controller/Optimizeip.php b/app/controller/Optimizeip.php index 3b33cd4..d7b8f9f 100644 --- a/app/controller/Optimizeip.php +++ b/app/controller/Optimizeip.php @@ -1,183 +1,183 @@ -alert('error', '无权限'); - if ($this->request->isPost()) { - $params = input('post.'); - foreach ($params as $key => $value) { - if (empty($key)) { - continue; - } - if ($key == 'optimize_ip_min' && intval($value) < 10) { - return json(['code' => -1, 'msg' => '自动更新时间间隔不能小于10分钟']); - } - config_set($key, $value); - Cache::delete('configs'); - } - return json(['code' => 0, 'msg' => 'succ']); - } - return View::fetch(); - } - - public function opiplist() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - return View::fetch(); - } - - public function opiplist_data() - { - if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); - $type = input('post.type/d', 1); - $kw = input('post.kw', null, 'trim'); - $status = input('post.status', null); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - - $select = Db::name('optimizeip')->alias('A')->join('domain B', 'A.did = B.id'); - if (!empty($kw)) { - if ($type == 1) { - $select->whereLike('rr|B.name', '%' . $kw . '%'); - } elseif ($type == 2) { - $select->whereLike('remark', '%' . $kw . '%'); - } - } - if (!isNullOrEmpty($status)) { - $select->where('status', intval($status)); - } - $total = $select->count(); - $list = $select->order('A.id', 'desc')->limit($offset, $limit)->field('A.*,B.name domain')->select(); - - return json(['total' => $total, 'rows' => $list]); - } - - public function opipform() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - if ($this->request->isPost()) { - if ($action == 'add') { - $task = [ - 'did' => input('post.did/d'), - 'rr' => input('post.rr', null, 'trim'), - 'type' => input('post.type/d'), - 'ip_type' => input('post.ip_type', null, 'trim'), - 'cdn_type' => input('post.cdn_type/d'), - 'recordnum' => input('post.recordnum/d'), - 'ttl' => input('post.ttl/d'), - 'remark' => input('post.remark', null, 'trim'), - 'addtime' => date('Y-m-d H:i:s'), - 'active' => 1 - ]; - - 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 (Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->find()) { - return json(['code' => -1, 'msg' => '当前域名的优选IP任务已存在']); - } - Db::name('optimizeip')->insert($task); - return json(['code' => 0, 'msg' => '添加成功']); - } elseif ($action == 'edit') { - $id = input('post.id/d'); - $task = [ - 'did' => input('post.did/d'), - 'rr' => input('post.rr', null, 'trim'), - 'type' => input('post.type/d'), - 'ip_type' => input('post.ip_type', null, 'trim'), - 'cdn_type' => input('post.cdn_type/d'), - 'recordnum' => input('post.recordnum/d'), - 'ttl' => input('post.ttl/d'), - 'remark' => input('post.remark', null, 'trim'), - ]; - - 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 (Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->where('id', '<>', $id)->find()) { - return json(['code' => -1, 'msg' => '当前域名的优选IP任务已存在']); - } - Db::name('optimizeip')->where('id', $id)->update($task); - return json(['code' => 0, 'msg' => '修改成功']); - } elseif ($action == 'setactive') { - $id = input('post.id/d'); - $active = input('post.active/d'); - Db::name('optimizeip')->where('id', $id)->update(['active' => $active]); - return json(['code' => 0, 'msg' => '设置成功']); - } elseif ($action == 'del') { - $id = input('post.id/d'); - Db::name('optimizeip')->where('id', $id)->delete(); - return json(['code' => 0, 'msg' => '删除成功']); - } elseif ($action == 'run') { - $id = input('post.id/d'); - $task = Db::name('optimizeip')->where('id', $id)->find(); - if (empty($task)) return json(['code' => -1, 'msg' => '任务不存在']); - try { - $result = (new OptimizeService())->execute_one($task); - Db::name('optimizeip')->where('id', $id)->update(['status' => 1, 'errmsg' => null, 'updatetime' => date('Y-m-d H:i:s')]); - return json(['code' => 0, 'msg' => '优选任务执行成功:' . $result]); - } catch (Exception $e) { - Db::name('optimizeip')->where('id', $id)->update(['status' => 2, 'errmsg' => $e->getMessage(), 'updatetime' => date('Y-m-d H:i:s')]); - return json(['code' => -1, 'msg' => '优选任务执行失败:' . $e->getMessage(), 'stack' => $e->__toString()]); - } - } else { - return json(['code' => -1, 'msg' => '参数错误']); - } - } - $task = null; - if ($action == 'edit') { - $id = input('get.id/d'); - $task = Db::name('optimizeip')->where('id', $id)->find(); - if (empty($task)) return $this->alert('error', '任务不存在'); - } - - $domains = []; - foreach (Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->field('A.*')->where('B.type', '<>', 'cloudflare')->select() as $row) { - $domains[$row['id']] = $row['name']; - } - View::assign('domains', $domains); - - View::assign('info', $task); - View::assign('action', $action); - return View::fetch(); - } - - public function queryapi() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $optimize_ip_api = input('post.optimize_ip_api/d'); - $optimize_ip_key = input('post.optimize_ip_key', null, 'trim'); - if (empty($optimize_ip_key)) return json(['code' => -1, 'msg' => '参数不能为空']); - try { - $result = (new OptimizeService())->get_license($optimize_ip_api, $optimize_ip_key); - return json(['code' => 0, 'msg' => '当前积分余额:' . $result]); - } catch (Exception $e) { - return json(['code' => -1, 'msg' => $e->getMessage()]); - } - } - - public function status() - { - $run_time = Db::name('optimizeip')->where('active', 1)->order('updatetime', 'desc')->value('updatetime'); - $run_state = $run_time ? (time() - strtotime($run_time) > 3600 ? 0 : 1) : 0; - return $run_state == 1 ? 'ok' : 'error'; - } -} +alert('error', '无权限'); + if ($this->request->isPost()) { + $params = input('post.'); + foreach ($params as $key => $value) { + if (empty($key)) { + continue; + } + if ($key == 'optimize_ip_min' && intval($value) < 10) { + return json(['code' => -1, 'msg' => '自动更新时间间隔不能小于10分钟']); + } + config_set($key, $value); + Cache::delete('configs'); + } + return json(['code' => 0, 'msg' => 'succ']); + } + return View::fetch(); + } + + public function opiplist() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + return View::fetch(); + } + + public function opiplist_data() + { + if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); + $type = input('post.type/d', 1); + $kw = input('post.kw', null, 'trim'); + $status = input('post.status', null); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('optimizeip')->alias('A')->join('domain B', 'A.did = B.id'); + if (!empty($kw)) { + if ($type == 1) { + $select->whereLike('rr|B.name', '%' . $kw . '%'); + } elseif ($type == 2) { + $select->whereLike('remark', '%' . $kw . '%'); + } + } + if (!isNullOrEmpty($status)) { + $select->where('status', intval($status)); + } + $total = $select->count(); + $list = $select->order('A.id', 'desc')->limit($offset, $limit)->field('A.*,B.name domain')->select(); + + return json(['total' => $total, 'rows' => $list]); + } + + public function opipform() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + if ($this->request->isPost()) { + if ($action == 'add') { + $task = [ + 'did' => input('post.did/d'), + 'rr' => input('post.rr', null, 'trim'), + 'type' => input('post.type/d'), + 'ip_type' => input('post.ip_type', null, 'trim'), + 'cdn_type' => input('post.cdn_type/d'), + 'recordnum' => input('post.recordnum/d'), + 'ttl' => input('post.ttl/d'), + 'remark' => input('post.remark', null, 'trim'), + 'addtime' => date('Y-m-d H:i:s'), + 'active' => 1 + ]; + + 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 (Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->find()) { + return json(['code' => -1, 'msg' => '当前域名的优选IP任务已存在']); + } + Db::name('optimizeip')->insert($task); + return json(['code' => 0, 'msg' => '添加成功']); + } elseif ($action == 'edit') { + $id = input('post.id/d'); + $task = [ + 'did' => input('post.did/d'), + 'rr' => input('post.rr', null, 'trim'), + 'type' => input('post.type/d'), + 'ip_type' => input('post.ip_type', null, 'trim'), + 'cdn_type' => input('post.cdn_type/d'), + 'recordnum' => input('post.recordnum/d'), + 'ttl' => input('post.ttl/d'), + 'remark' => input('post.remark', null, 'trim'), + ]; + + 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 (Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->where('id', '<>', $id)->find()) { + return json(['code' => -1, 'msg' => '当前域名的优选IP任务已存在']); + } + Db::name('optimizeip')->where('id', $id)->update($task); + return json(['code' => 0, 'msg' => '修改成功']); + } elseif ($action == 'setactive') { + $id = input('post.id/d'); + $active = input('post.active/d'); + Db::name('optimizeip')->where('id', $id)->update(['active' => $active]); + return json(['code' => 0, 'msg' => '设置成功']); + } elseif ($action == 'del') { + $id = input('post.id/d'); + Db::name('optimizeip')->where('id', $id)->delete(); + return json(['code' => 0, 'msg' => '删除成功']); + } elseif ($action == 'run') { + $id = input('post.id/d'); + $task = Db::name('optimizeip')->where('id', $id)->find(); + if (empty($task)) return json(['code' => -1, 'msg' => '任务不存在']); + try { + $result = (new OptimizeService())->execute_one($task); + Db::name('optimizeip')->where('id', $id)->update(['status' => 1, 'errmsg' => null, 'updatetime' => date('Y-m-d H:i:s')]); + return json(['code' => 0, 'msg' => '优选任务执行成功:' . $result]); + } catch (Exception $e) { + Db::name('optimizeip')->where('id', $id)->update(['status' => 2, 'errmsg' => $e->getMessage(), 'updatetime' => date('Y-m-d H:i:s')]); + return json(['code' => -1, 'msg' => '优选任务执行失败:' . $e->getMessage(), 'stack' => $e->__toString()]); + } + } else { + return json(['code' => -1, 'msg' => '参数错误']); + } + } + $task = null; + if ($action == 'edit') { + $id = input('get.id/d'); + $task = Db::name('optimizeip')->where('id', $id)->find(); + if (empty($task)) return $this->alert('error', '任务不存在'); + } + + $domains = []; + foreach (Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->field('A.*')->where('B.type', '<>', 'cloudflare')->select() as $row) { + $domains[$row['id']] = $row['name']; + } + View::assign('domains', $domains); + + View::assign('info', $task); + View::assign('action', $action); + return View::fetch(); + } + + public function queryapi() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $optimize_ip_api = input('post.optimize_ip_api/d'); + $optimize_ip_key = input('post.optimize_ip_key', null, 'trim'); + if (empty($optimize_ip_key)) return json(['code' => -1, 'msg' => '参数不能为空']); + try { + $result = (new OptimizeService())->get_license($optimize_ip_api, $optimize_ip_key); + return json(['code' => 0, 'msg' => '当前积分余额:' . $result]); + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } + + public function status() + { + $run_time = Db::name('optimizeip')->where('active', 1)->order('updatetime', 'desc')->value('updatetime'); + $run_state = $run_time ? (time() - strtotime($run_time) > 3600 ? 0 : 1) : 0; + return $run_state == 1 ? 'ok' : 'error'; + } +} diff --git a/app/controller/Schedule.php b/app/controller/Schedule.php index dace308..8350be4 100644 --- a/app/controller/Schedule.php +++ b/app/controller/Schedule.php @@ -1,165 +1,165 @@ -alert('error', '无权限'); - return View::fetch(); - } - - public function stask_data() - { - if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); - $type = input('post.type/d', 1); - $kw = input('post.kw', null, 'trim'); - $stype = input('post.stype', null); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - - $select = Db::name('sctask')->alias('A')->join('domain B', 'A.did = B.id'); - if (!empty($kw)) { - if ($type == 1) { - $select->whereLike('rr|B.name', '%' . $kw . '%'); - } elseif ($type == 2) { - $select->where('recordid', $kw); - } elseif ($type == 3) { - $select->where('value', $kw); - } elseif ($type == 4) { - $select->whereLike('remark', '%' . $kw . '%'); - } - } - if (!isNullOrEmpty($stype)) { - $select->where('type', $stype); - } - $total = $select->count(); - $list = $select->order('A.id', 'desc')->limit($offset, $limit)->field('A.*,B.name domain')->select()->toArray(); - - foreach ($list as &$row) { - $row['addtimestr'] = date('Y-m-d H:i:s', $row['addtime']); - $row['updatetimestr'] = $row['updatetime'] > 0 ? date('Y-m-d H:i:s', $row['updatetime']) : '未运行'; - $row['nexttimestr'] = $row['nexttime'] > 0 ? date('Y-m-d H:i:s', $row['nexttime']) : '无'; - } - - return json(['total' => $total, 'rows' => $list]); - } - - public function stask_op() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - if ($action == 'add') { - $task = [ - 'did' => input('post.did/d'), - 'rr' => input('post.rr', null, 'trim'), - 'recordid' => input('post.recordid', null, 'trim'), - 'type' => input('post.type/d'), - 'cycle' => input('post.cycle/d'), - 'switchtype' => input('post.switchtype/d'), - 'switchdate' => input('post.switchdate', null, 'trim'), - 'switchtime' => input('post.switchtime', null, 'trim'), - 'value' => input('post.value', null, 'trim'), - 'line' => input('post.line', null, 'trim'), - 'remark' => input('post.remark', null, 'trim'), - 'recordinfo' => input('post.recordinfo', null, 'trim'), - 'addtime' => time(), - 'active' => 1 - ]; - - if (empty($task['did']) || empty($task['rr']) || empty($task['recordid'])) { - return json(['code' => -1, 'msg' => '必填项不能为空']); - } - if (Db::name('sctask')->where('recordid', $task['recordid'])->where('switchtype', $task['switchtype'])->where('switchtime', $task['switchtime'])->find()) { - return json(['code' => -1, 'msg' => '当前定时切换策略已存在']); - } - $id = Db::name('sctask')->insertGetId($task); - $row = Db::name('sctask')->where('id', $id)->find(); - (new ScheduleService())->update_nexttime($row); - return json(['code' => 0, 'msg' => '添加成功']); - } elseif ($action == 'edit') { - $id = input('post.id/d'); - $task = [ - 'did' => input('post.did/d'), - 'rr' => input('post.rr', null, 'trim'), - 'recordid' => input('post.recordid', null, 'trim'), - 'type' => input('post.type/d'), - 'cycle' => input('post.cycle/d'), - 'switchtype' => input('post.switchtype/d'), - 'switchdate' => input('post.switchdate', null, 'trim'), - 'switchtime' => input('post.switchtime', null, 'trim'), - 'value' => input('post.value', null, 'trim'), - 'line' => input('post.line', null, 'trim'), - 'remark' => input('post.remark', null, 'trim'), - 'recordinfo' => input('post.recordinfo', null, 'trim'), - ]; - - if (empty($task['did']) || empty($task['rr']) || empty($task['recordid'])) { - return json(['code' => -1, 'msg' => '必填项不能为空']); - } - if (Db::name('sctask')->where('recordid', $task['recordid'])->where('switchtype', $task['switchtype'])->where('switchtime', $task['switchtime'])->where('id', '<>', $id)->find()) { - return json(['code' => -1, 'msg' => '当前定时切换策略已存在']); - } - Db::name('sctask')->where('id', $id)->update($task); - $row = Db::name('sctask')->where('id', $id)->find(); - (new ScheduleService())->update_nexttime($row); - return json(['code' => 0, 'msg' => '修改成功']); - } elseif ($action == 'setactive') { - $id = input('post.id/d'); - $active = input('post.active/d'); - Db::name('sctask')->where('id', $id)->update(['active' => $active]); - return json(['code' => 0, 'msg' => '设置成功']); - } elseif ($action == 'del') { - $id = input('post.id/d'); - Db::name('sctask')->where('id', $id)->delete(); - return json(['code' => 0, 'msg' => '删除成功']); - } elseif ($action == 'operation') { - $ids = input('post.ids'); - $success = 0; - foreach ($ids as $id) { - if (input('post.act') == 'delete') { - Db::name('sctask')->where('id', $id)->delete(); - $success++; - } elseif (input('post.act') == 'open' || input('post.act') == 'close') { - $isauto = input('post.act') == 'open' ? 1 : 0; - Db::name('sctask')->where('id', $id)->update(['active' => $isauto]); - $success++; - } - } - return json(['code' => 0, 'msg' => '成功操作' . $success . '个定时切换策略']); - } else { - return json(['code' => -1, 'msg' => '参数错误']); - } - } - - public function staskform() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $action = input('param.action'); - $task = null; - if ($action == 'edit') { - $id = input('get.id/d'); - $task = Db::name('sctask')->where('id', $id)->find(); - if (empty($task)) return $this->alert('error', '切换策略不存在'); - } - - $domains = []; - $domainList = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->field('A.id,A.name,B.type')->select(); - foreach ($domainList as $row) { - $domains[] = ['id'=>$row['id'], 'name'=>$row['name'], 'type'=>$row['type']]; - } - View::assign('domains', $domains); - - View::assign('info', $task); - View::assign('action', $action); - return View::fetch(); - } - -} +alert('error', '无权限'); + return View::fetch(); + } + + public function stask_data() + { + if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); + $type = input('post.type/d', 1); + $kw = input('post.kw', null, 'trim'); + $stype = input('post.stype', null); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('sctask')->alias('A')->join('domain B', 'A.did = B.id'); + if (!empty($kw)) { + if ($type == 1) { + $select->whereLike('rr|B.name', '%' . $kw . '%'); + } elseif ($type == 2) { + $select->where('recordid', $kw); + } elseif ($type == 3) { + $select->where('value', $kw); + } elseif ($type == 4) { + $select->whereLike('remark', '%' . $kw . '%'); + } + } + if (!isNullOrEmpty($stype)) { + $select->where('type', $stype); + } + $total = $select->count(); + $list = $select->order('A.id', 'desc')->limit($offset, $limit)->field('A.*,B.name domain')->select()->toArray(); + + foreach ($list as &$row) { + $row['addtimestr'] = date('Y-m-d H:i:s', $row['addtime']); + $row['updatetimestr'] = $row['updatetime'] > 0 ? date('Y-m-d H:i:s', $row['updatetime']) : '未运行'; + $row['nexttimestr'] = $row['nexttime'] > 0 ? date('Y-m-d H:i:s', $row['nexttime']) : '无'; + } + + return json(['total' => $total, 'rows' => $list]); + } + + public function stask_op() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + if ($action == 'add') { + $task = [ + 'did' => input('post.did/d'), + 'rr' => input('post.rr', null, 'trim'), + 'recordid' => input('post.recordid', null, 'trim'), + 'type' => input('post.type/d'), + 'cycle' => input('post.cycle/d'), + 'switchtype' => input('post.switchtype/d'), + 'switchdate' => input('post.switchdate', null, 'trim'), + 'switchtime' => input('post.switchtime', null, 'trim'), + 'value' => input('post.value', null, 'trim'), + 'line' => input('post.line', null, 'trim'), + 'remark' => input('post.remark', null, 'trim'), + 'recordinfo' => input('post.recordinfo', null, 'trim'), + 'addtime' => time(), + 'active' => 1 + ]; + + if (empty($task['did']) || empty($task['rr']) || empty($task['recordid'])) { + return json(['code' => -1, 'msg' => '必填项不能为空']); + } + if (Db::name('sctask')->where('recordid', $task['recordid'])->where('switchtype', $task['switchtype'])->where('switchtime', $task['switchtime'])->find()) { + return json(['code' => -1, 'msg' => '当前定时切换策略已存在']); + } + $id = Db::name('sctask')->insertGetId($task); + $row = Db::name('sctask')->where('id', $id)->find(); + (new ScheduleService())->update_nexttime($row); + return json(['code' => 0, 'msg' => '添加成功']); + } elseif ($action == 'edit') { + $id = input('post.id/d'); + $task = [ + 'did' => input('post.did/d'), + 'rr' => input('post.rr', null, 'trim'), + 'recordid' => input('post.recordid', null, 'trim'), + 'type' => input('post.type/d'), + 'cycle' => input('post.cycle/d'), + 'switchtype' => input('post.switchtype/d'), + 'switchdate' => input('post.switchdate', null, 'trim'), + 'switchtime' => input('post.switchtime', null, 'trim'), + 'value' => input('post.value', null, 'trim'), + 'line' => input('post.line', null, 'trim'), + 'remark' => input('post.remark', null, 'trim'), + 'recordinfo' => input('post.recordinfo', null, 'trim'), + ]; + + if (empty($task['did']) || empty($task['rr']) || empty($task['recordid'])) { + return json(['code' => -1, 'msg' => '必填项不能为空']); + } + if (Db::name('sctask')->where('recordid', $task['recordid'])->where('switchtype', $task['switchtype'])->where('switchtime', $task['switchtime'])->where('id', '<>', $id)->find()) { + return json(['code' => -1, 'msg' => '当前定时切换策略已存在']); + } + Db::name('sctask')->where('id', $id)->update($task); + $row = Db::name('sctask')->where('id', $id)->find(); + (new ScheduleService())->update_nexttime($row); + return json(['code' => 0, 'msg' => '修改成功']); + } elseif ($action == 'setactive') { + $id = input('post.id/d'); + $active = input('post.active/d'); + Db::name('sctask')->where('id', $id)->update(['active' => $active]); + return json(['code' => 0, 'msg' => '设置成功']); + } elseif ($action == 'del') { + $id = input('post.id/d'); + Db::name('sctask')->where('id', $id)->delete(); + return json(['code' => 0, 'msg' => '删除成功']); + } elseif ($action == 'operation') { + $ids = input('post.ids'); + $success = 0; + foreach ($ids as $id) { + if (input('post.act') == 'delete') { + Db::name('sctask')->where('id', $id)->delete(); + $success++; + } elseif (input('post.act') == 'open' || input('post.act') == 'close') { + $isauto = input('post.act') == 'open' ? 1 : 0; + Db::name('sctask')->where('id', $id)->update(['active' => $isauto]); + $success++; + } + } + return json(['code' => 0, 'msg' => '成功操作' . $success . '个定时切换策略']); + } else { + return json(['code' => -1, 'msg' => '参数错误']); + } + } + + public function staskform() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + $task = null; + if ($action == 'edit') { + $id = input('get.id/d'); + $task = Db::name('sctask')->where('id', $id)->find(); + if (empty($task)) return $this->alert('error', '切换策略不存在'); + } + + $domains = []; + $domainList = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->field('A.id,A.name,B.type')->select(); + foreach ($domainList as $row) { + $domains[] = ['id'=>$row['id'], 'name'=>$row['name'], 'type'=>$row['type']]; + } + View::assign('domains', $domains); + + View::assign('info', $task); + View::assign('action', $action); + return View::fetch(); + } + +} diff --git a/app/controller/System.php b/app/controller/System.php index fd5dbd7..0e2ebf3 100644 --- a/app/controller/System.php +++ b/app/controller/System.php @@ -1,149 +1,149 @@ -alert('error', '无权限'); - $params = input('post.'); - if (isset($params['mail_type']) && isset($params['mail_name2']) && $params['mail_type'] > 0) { - $params['mail_name'] = $params['mail_name2']; - unset($params['mail_name2']); - } - foreach ($params as $key => $value) { - if (empty($key)) { - continue; - } - config_set($key, $value); - } - Cache::delete('configs'); - return json(['code' => 0, 'msg' => 'succ']); - } - - public function loginset() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - return View::fetch(); - } - - public function noticeset() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - return View::fetch(); - } - - public function proxyset() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - return View::fetch(); - } - - public function mailtest() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); - if (empty($mail_name)) return json(['code' => -1, 'msg' => '您还未设置邮箱!']); - $result = \app\utils\MsgNotice::send_mail($mail_name, '邮件发送测试。', '这是一封测试邮件!

来自:' . $this->request->root(true)); - if ($result === true) { - return json(['code' => 0, 'msg' => '邮件发送成功!']); - } else { - return json(['code' => -1, 'msg' => '邮件发送失败!' . $result]); - } - } - - public function tgbottest() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $tgbot_token = config_get('tgbot_token'); - $tgbot_chatid = config_get('tgbot_chatid'); - if (empty($tgbot_token) || empty($tgbot_chatid)) return json(['code' => -1, 'msg' => '请先保存设置']); - $content = "消息发送测试\n\n这是一封测试消息!\n\n来自:" . $this->request->root(true); - $result = \app\utils\MsgNotice::send_telegram_bot($content); - if ($result === true) { - return json(['code' => 0, 'msg' => '消息发送成功!']); - } else { - return json(['code' => -1, 'msg' => '消息发送失败!' . $result]); - } - } - - public function webhooktest() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $webhook_url = config_get('webhook_url'); - if (empty($webhook_url)) return json(['code' => -1, 'msg' => '请先保存设置']); - $content = "这是一封测试消息!\n来自:" . $this->request->root(true); - $result = \app\utils\MsgNotice::send_webhook('消息发送测试', $content); - if ($result === true) { - return json(['code' => 0, 'msg' => '消息发送成功!']); - } else { - return json(['code' => -1, 'msg' => '消息发送失败!' . $result]); - } - } - - public function proxytest() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $proxy_server = trim($_POST['proxy_server']); - $proxy_port = $_POST['proxy_port']; - $proxy_user = trim($_POST['proxy_user']); - $proxy_pwd = trim($_POST['proxy_pwd']); - $proxy_type = $_POST['proxy_type']; - try { - check_proxy('https://dl.amh.sh/ip.htm', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd); - } catch (Exception $e) { - try { - check_proxy('https://myip.ipip.net/', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd); - } catch (Exception $e) { - return json(['code' => -1, 'msg' => $e->getMessage()]); - } - } - return json(['code' => 0]); - } - - public function cronset() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - if (config_get('cron_key') === null) { - config_set('cron_key', random(10)); - Cache::delete('configs'); - } - View::assign('is_user_www', isset($_SERVER['USER']) && $_SERVER['USER'] == 'www'); - View::assign('siteurl', request()->root(true)); - return View::fetch(); - } - - public function cron() - { - if (function_exists("set_time_limit")) { - @set_time_limit(0); - } - if (function_exists("ignore_user_abort")) { - @ignore_user_abort(true); - } - if (isset($_SERVER['HTTP_USER_AGENT']) && str_contains($_SERVER['HTTP_USER_AGENT'], 'Baiduspider')) exit; - $key = input('get.key', ''); - $cron_key = config_get('cron_key'); - if (config_get('cron_type', '0') != '1' || empty($cron_key)) exit('未开启当前方式'); - if ($key != $cron_key) exit('访问密钥错误'); - - (new ScheduleService())->execute(); - $res = (new OptimizeService())->execute(); - if (!$res) { - (new CertTaskService())->execute(); - (new ExpireNoticeService())->task(); - } - echo 'success!'; - } +alert('error', '无权限'); + $params = input('post.'); + if (isset($params['mail_type']) && isset($params['mail_name2']) && $params['mail_type'] > 0) { + $params['mail_name'] = $params['mail_name2']; + unset($params['mail_name2']); + } + foreach ($params as $key => $value) { + if (empty($key)) { + continue; + } + config_set($key, $value); + } + Cache::delete('configs'); + return json(['code' => 0, 'msg' => 'succ']); + } + + public function loginset() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + return View::fetch(); + } + + public function noticeset() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + return View::fetch(); + } + + public function proxyset() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + return View::fetch(); + } + + public function mailtest() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); + if (empty($mail_name)) return json(['code' => -1, 'msg' => '您还未设置邮箱!']); + $result = \app\utils\MsgNotice::send_mail($mail_name, '邮件发送测试。', '这是一封测试邮件!

来自:' . $this->request->root(true)); + if ($result === true) { + return json(['code' => 0, 'msg' => '邮件发送成功!']); + } else { + return json(['code' => -1, 'msg' => '邮件发送失败!' . $result]); + } + } + + public function tgbottest() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $tgbot_token = config_get('tgbot_token'); + $tgbot_chatid = config_get('tgbot_chatid'); + if (empty($tgbot_token) || empty($tgbot_chatid)) return json(['code' => -1, 'msg' => '请先保存设置']); + $content = "消息发送测试\n\n这是一封测试消息!\n\n来自:" . $this->request->root(true); + $result = \app\utils\MsgNotice::send_telegram_bot($content); + if ($result === true) { + return json(['code' => 0, 'msg' => '消息发送成功!']); + } else { + return json(['code' => -1, 'msg' => '消息发送失败!' . $result]); + } + } + + public function webhooktest() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $webhook_url = config_get('webhook_url'); + if (empty($webhook_url)) return json(['code' => -1, 'msg' => '请先保存设置']); + $content = "这是一封测试消息!\n来自:" . $this->request->root(true); + $result = \app\utils\MsgNotice::send_webhook('消息发送测试', $content); + if ($result === true) { + return json(['code' => 0, 'msg' => '消息发送成功!']); + } else { + return json(['code' => -1, 'msg' => '消息发送失败!' . $result]); + } + } + + public function proxytest() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $proxy_server = trim($_POST['proxy_server']); + $proxy_port = $_POST['proxy_port']; + $proxy_user = trim($_POST['proxy_user']); + $proxy_pwd = trim($_POST['proxy_pwd']); + $proxy_type = $_POST['proxy_type']; + try { + check_proxy('https://dl.amh.sh/ip.htm', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd); + } catch (Exception $e) { + try { + check_proxy('https://myip.ipip.net/', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd); + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } + return json(['code' => 0]); + } + + public function cronset() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + if (config_get('cron_key') === null) { + config_set('cron_key', random(10)); + Cache::delete('configs'); + } + View::assign('is_user_www', isset($_SERVER['USER']) && $_SERVER['USER'] == 'www'); + View::assign('siteurl', request()->root(true)); + return View::fetch(); + } + + public function cron() + { + if (function_exists("set_time_limit")) { + @set_time_limit(0); + } + if (function_exists("ignore_user_abort")) { + @ignore_user_abort(true); + } + if (isset($_SERVER['HTTP_USER_AGENT']) && str_contains($_SERVER['HTTP_USER_AGENT'], 'Baiduspider')) exit; + $key = input('get.key', ''); + $cron_key = config_get('cron_key'); + if (config_get('cron_type', '0') != '1' || empty($cron_key)) exit('未开启当前方式'); + if ($key != $cron_key) exit('访问密钥错误'); + + (new ScheduleService())->execute(); + $res = (new OptimizeService())->execute(); + if (!$res) { + (new CertTaskService())->execute(); + (new ExpireNoticeService())->task(); + } + echo 'success!'; + } } \ No newline at end of file diff --git a/app/controller/User.php b/app/controller/User.php index 1c58560..91242ce 100644 --- a/app/controller/User.php +++ b/app/controller/User.php @@ -1,188 +1,188 @@ -alert('error', '无权限'); - $list = Db::name('domain')->select(); - $domains = []; - foreach ($list as $row) { - $domains[] = $row['name']; - } - View::assign('domains', $domains); - return view(); - } - - public function user_data() - { - if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); - $kw = input('post.kw', null, 'trim'); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - - $select = Db::name('user'); - if (!empty($kw)) { - $select->whereLike('id|username', $kw); - } - $total = $select->count(); - $rows = $select->order('id', 'desc')->limit($offset, $limit)->select(); - - return json(['total' => $total, 'rows' => $rows]); - } - - public function user_op() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $act = input('param.act'); - if ($act == 'get') { - $id = input('post.id/d'); - $row = Db::name('user')->where('id', $id)->find(); - if (!$row) { - return json(['code' => -1, 'msg' => '用户不存在']); - } - $row['permission'] = Db::name('permission')->where('uid', $id)->column('domain'); - return json(['code' => 0, 'data' => $row]); - } elseif ($act == 'add') { - $username = input('post.username', null, 'trim'); - $password = input('post.password', null, 'trim'); - $is_api = input('post.is_api/d'); - $apikey = input('post.apikey', null, 'trim'); - $level = input('post.level/d'); - if (empty($username) || empty($password)) { - return json(['code' => -1, 'msg' => '用户名或密码不能为空']); - } - if ($is_api == 1 && empty($apikey)) { - return json(['code' => -1, 'msg' => 'API密钥不能为空']); - } - if (Db::name('user')->where('username', $username)->find()) { - return json(['code' => -1, 'msg' => '用户名已存在']); - } - $uid = Db::name('user')->insertGetId([ - 'username' => $username, - 'password' => password_hash($password, PASSWORD_DEFAULT), - 'is_api' => $is_api, - 'apikey' => $apikey, - 'level' => $level, - 'regtime' => date('Y-m-d H:i:s'), - 'status' => 1, - ]); - if ($level == 1) { - $permission = input('post.permission/a'); - if (!empty($permission)) { - $data = []; - foreach ($permission as $domain) { - $data[] = ['uid' => $uid, 'domain' => $domain]; - } - Db::name('permission')->insertAll($data); - } - } - return json(['code' => 0, 'msg' => '添加用户成功!']); - } elseif ($act == 'edit') { - $id = input('post.id/d'); - $row = Db::name('user')->where('id', $id)->find(); - if (!$row) return json(['code' => -1, 'msg' => '用户不存在']); - $username = input('post.username', null, 'trim'); - $is_api = input('post.is_api/d'); - $apikey = input('post.apikey', null, 'trim'); - $level = input('post.level/d'); - $repwd = input('post.repwd', null, 'trim'); - if (empty($username)) { - return json(['code' => -1, 'msg' => '用户名不能为空']); - } - if ($is_api == 1 && empty($apikey)) { - return json(['code' => -1, 'msg' => 'API密钥不能为空']); - } - if (Db::name('user')->where('username', $username)->where('id', '<>', $id)->find()) { - return json(['code' => -1, 'msg' => '用户名已存在']); - } - if ($level == 1 && ($id == 1000 || $id == $this->request->user['id'])) { - $level = 2; - } - Db::name('user')->where('id', $id)->update([ - 'username' => $username, - 'is_api' => $is_api, - 'apikey' => $apikey, - 'level' => $level, - ]); - Db::name('permission')->where(['uid' => $id])->delete(); - if ($level == 1) { - $permission = input('post.permission/a'); - if (!empty($permission)) { - $data = []; - foreach ($permission as $domain) { - $data[] = ['uid' => $id, 'domain' => $domain]; - } - Db::name('permission')->insertAll($data); - } - } - if (!empty($repwd)) { - Db::name('user')->where('id', $id)->update(['password' => password_hash($repwd, PASSWORD_DEFAULT)]); - } - return json(['code' => 0, 'msg' => '修改用户成功!']); - } elseif ($act == 'set') { - $id = input('post.id/d'); - $status = input('post.status/d'); - if ($id == 1000) { - return json(['code' => -1, 'msg' => '此用户无法修改状态']); - } - if ($id == $this->request->user['id']) { - return json(['code' => -1, 'msg' => '当前登录用户无法修改状态']); - } - Db::name('user')->where('id', $id)->update(['status' => $status]); - return json(['code' => 0]); - } elseif ($act == 'del') { - $id = input('post.id/d'); - if ($id == 1000) { - return json(['code' => -1, 'msg' => '此用户无法删除']); - } - if ($id == $this->request->user['id']) { - return json(['code' => -1, 'msg' => '当前登录用户无法删除']); - } - Db::name('user')->where('id', $id)->delete(); - return json(['code' => 0]); - } - return json(['code' => -3]); - } - - public function log() - { - return view(); - } - - public function log_data() - { - $uid = input('post.uid', null, 'trim'); - $kw = input('post.kw', null, 'trim'); - $domain = input('post.domain', null, 'trim'); - $offset = input('post.offset/d'); - $limit = input('post.limit/d'); - - $select = Db::name('log'); - if ($this->request->user['type'] == 'domain') { - $select->where('domain', $this->request->user['name']); - } elseif ($this->request->user['level'] == 1) { - $select->where('uid', $this->request->user['id']); - } elseif (!isNullOrEmpty($uid)) { - $select->where('uid', $uid); - } - if (!empty($kw)) { - $select->whereLike('action|data', '%' . $kw . '%'); - } - if (!empty($domain)) { - $select->where('domain', $domain); - } - $total = $select->count(); - $rows = $select->order('id', 'desc')->limit($offset, $limit)->select(); - - return json(['total' => $total, 'rows' => $rows]); - } -} +alert('error', '无权限'); + $list = Db::name('domain')->select(); + $domains = []; + foreach ($list as $row) { + $domains[] = $row['name']; + } + View::assign('domains', $domains); + return view(); + } + + public function user_data() + { + if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]); + $kw = input('post.kw', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('user'); + if (!empty($kw)) { + $select->whereLike('id|username', $kw); + } + $total = $select->count(); + $rows = $select->order('id', 'desc')->limit($offset, $limit)->select(); + + return json(['total' => $total, 'rows' => $rows]); + } + + public function user_op() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $act = input('param.act'); + if ($act == 'get') { + $id = input('post.id/d'); + $row = Db::name('user')->where('id', $id)->find(); + if (!$row) { + return json(['code' => -1, 'msg' => '用户不存在']); + } + $row['permission'] = Db::name('permission')->where('uid', $id)->column('domain'); + return json(['code' => 0, 'data' => $row]); + } elseif ($act == 'add') { + $username = input('post.username', null, 'trim'); + $password = input('post.password', null, 'trim'); + $is_api = input('post.is_api/d'); + $apikey = input('post.apikey', null, 'trim'); + $level = input('post.level/d'); + if (empty($username) || empty($password)) { + return json(['code' => -1, 'msg' => '用户名或密码不能为空']); + } + if ($is_api == 1 && empty($apikey)) { + return json(['code' => -1, 'msg' => 'API密钥不能为空']); + } + if (Db::name('user')->where('username', $username)->find()) { + return json(['code' => -1, 'msg' => '用户名已存在']); + } + $uid = Db::name('user')->insertGetId([ + 'username' => $username, + 'password' => password_hash($password, PASSWORD_DEFAULT), + 'is_api' => $is_api, + 'apikey' => $apikey, + 'level' => $level, + 'regtime' => date('Y-m-d H:i:s'), + 'status' => 1, + ]); + if ($level == 1) { + $permission = input('post.permission/a'); + if (!empty($permission)) { + $data = []; + foreach ($permission as $domain) { + $data[] = ['uid' => $uid, 'domain' => $domain]; + } + Db::name('permission')->insertAll($data); + } + } + return json(['code' => 0, 'msg' => '添加用户成功!']); + } elseif ($act == 'edit') { + $id = input('post.id/d'); + $row = Db::name('user')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => '用户不存在']); + $username = input('post.username', null, 'trim'); + $is_api = input('post.is_api/d'); + $apikey = input('post.apikey', null, 'trim'); + $level = input('post.level/d'); + $repwd = input('post.repwd', null, 'trim'); + if (empty($username)) { + return json(['code' => -1, 'msg' => '用户名不能为空']); + } + if ($is_api == 1 && empty($apikey)) { + return json(['code' => -1, 'msg' => 'API密钥不能为空']); + } + if (Db::name('user')->where('username', $username)->where('id', '<>', $id)->find()) { + return json(['code' => -1, 'msg' => '用户名已存在']); + } + if ($level == 1 && ($id == 1000 || $id == $this->request->user['id'])) { + $level = 2; + } + Db::name('user')->where('id', $id)->update([ + 'username' => $username, + 'is_api' => $is_api, + 'apikey' => $apikey, + 'level' => $level, + ]); + Db::name('permission')->where(['uid' => $id])->delete(); + if ($level == 1) { + $permission = input('post.permission/a'); + if (!empty($permission)) { + $data = []; + foreach ($permission as $domain) { + $data[] = ['uid' => $id, 'domain' => $domain]; + } + Db::name('permission')->insertAll($data); + } + } + if (!empty($repwd)) { + Db::name('user')->where('id', $id)->update(['password' => password_hash($repwd, PASSWORD_DEFAULT)]); + } + return json(['code' => 0, 'msg' => '修改用户成功!']); + } elseif ($act == 'set') { + $id = input('post.id/d'); + $status = input('post.status/d'); + if ($id == 1000) { + return json(['code' => -1, 'msg' => '此用户无法修改状态']); + } + if ($id == $this->request->user['id']) { + return json(['code' => -1, 'msg' => '当前登录用户无法修改状态']); + } + Db::name('user')->where('id', $id)->update(['status' => $status]); + return json(['code' => 0]); + } elseif ($act == 'del') { + $id = input('post.id/d'); + if ($id == 1000) { + return json(['code' => -1, 'msg' => '此用户无法删除']); + } + if ($id == $this->request->user['id']) { + return json(['code' => -1, 'msg' => '当前登录用户无法删除']); + } + Db::name('user')->where('id', $id)->delete(); + return json(['code' => 0]); + } + return json(['code' => -3]); + } + + public function log() + { + return view(); + } + + public function log_data() + { + $uid = input('post.uid', null, 'trim'); + $kw = input('post.kw', null, 'trim'); + $domain = input('post.domain', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('log'); + if ($this->request->user['type'] == 'domain') { + $select->where('domain', $this->request->user['name']); + } elseif ($this->request->user['level'] == 1) { + $select->where('uid', $this->request->user['id']); + } elseif (!isNullOrEmpty($uid)) { + $select->where('uid', $uid); + } + if (!empty($kw)) { + $select->whereLike('action|data', '%' . $kw . '%'); + } + if (!empty($domain)) { + $select->where('domain', $domain); + } + $total = $select->count(); + $rows = $select->order('id', 'desc')->limit($offset, $limit)->select(); + + return json(['total' => $total, 'rows' => $rows]); + } +} diff --git a/app/data/domain_root.txt b/app/data/domain_root.txt index ce068e2..2554f4f 100644 --- a/app/data/domain_root.txt +++ b/app/data/domain_root.txt @@ -1,1944 +1,1944 @@ -ac.ae -co.ae -net.ae -org.ae -sch.ae -airport.aero -cargo.aero -charter.aero -com.af -edu.af -gov.af -net.af -org.af -co.ag -com.ag -net.ag -nom.ag -org.ag -com.ai -net.ai -off.ai -org.ai -com.al -net.al -org.al -co.am -com.am -net.am -north.am -org.am -radio.am -south.am -co.ao -it.ao -og.ao -pb.ao -com.ar -int.ar -net.ar -org.ar -co.at -or.at -asn.au -com.au -id.au -info.au -net.au -org.au -biz.az -co.az -com.az -edu.az -gov.az -info.az -int.az -mil.az -name.az -net.az -org.az -pp.az -pro.az -co.ba -co.bb -com.bb -net.bb -org.bb -ac.bd -com.bd -net.bd -org.bd -com.bh -co.bi -com.bi -edu.bi -info.bi -mo.bi -net.bi -or.bi -org.bi -auz.biz -com.bj -edu.bj -com.bm -net.bm -org.bm -com.bn -com.bo -net.bo -org.bo -tv.bo -abc.br -adm.br -adv.br -agr.br -am.br -aparecida.br -arq.br -art.br -ato.br -belem.br -bhz.br -bio.br -blog.br -bmd.br -boavista.br -bsb.br -campinas.br -caxias.br -cim.br -cng.br -cnt.br -com.br -coop.br -curitiba.br -ecn.br -eco.br -emp.br -eng.br -esp.br -etc.br -eti.br -far.br -flog.br -floripa.br -fm.br -fnd.br -fortal.br -fot.br -foz.br -fst.br -g12.br -ggf.br -gru.br -imb.br -ind.br -inf.br -jampa.br -jor.br -lel.br -macapa.br -maceio.br -manaus.br -mat.br -med.br -mil.br -mus.br -natal.br -net.br -nom.br -not.br -ntr.br -odo.br -org.br -palmas.br -poa.br -ppg.br -pro.br -psc.br -psi.br -qsl.br -radio.br -rec.br -recife.br -rep.br -rio.br -salvador.br -sjc.br -slg.br -srv.br -taxi.br -teo.br -tmp.br -trd.br -tur.br -tv.br -vet.br -vix.br -vlog.br -wiki.br -zlg.br -com.bs -net.bs -org.bs -com.bt -org.bt -ac.bw -co.bw -net.bw -org.bw -com.by -minsk.by -net.by -co.bz -com.bz -net.bz -org.bz -za.bz -com.cd -net.cd -org.cd -ac.ci -co.ci -com.ci -ed.ci -edu.ci -go.ci -in.ci -int.ci -net.ci -nom.ci -or.ci -org.ci -biz.ck -co.ck -edu.ck -gen.ck -gov.ck -info.ck -net.ck -org.ck -co.cm -com.cm -net.cm -ac.cn -ah.cn -bj.cn -com.cn -cq.cn -fj.cn -gd.cn -gs.cn -gx.cn -gz.cn -ha.cn -hb.cn -he.cn -hi.cn -hk.cn -hl.cn -hn.cn -jl.cn -js.cn -jx.cn -ln.cn -mo.cn -net.cn -nm.cn -nx.cn -org.cn -qh.cn -sc.cn -sd.cn -sh.cn -sn.cn -sx.cn -tj.cn -tw.cn -xj.cn -xz.cn -yn.cn -zj.cn -com.co -net.co -nom.co -ae.com -africa.com -ar.com -br.com -cn.com -co.com -de.com -eu.com -gb.com -gr.com -hk.com -hu.com -jpn.com -kr.com -mex.com -no.com -nv.com -pty-ltd.com -qc.com -ru.com -sa.com -se.com -uk.com -us.com -uy.com -za.com -it.com -co.cr -ed.cr -fi.cr -go.cr -or.cr -sa.cr -com.cu -com.cv -int.cv -net.cv -nome.cv -org.cv -publ.cv -com.cw -net.cw -ac.cy -biz.cy -com.cy -ekloges.cy -ltd.cy -name.cy -net.cy -org.cy -parliament.cy -press.cy -pro.cy -tm.cy -co.cz -co.de -com.de -biz.dk -co.dk -co.dm -com.dm -net.dm -org.dm -art.do -com.do -net.do -org.do -sld.do -web.do -com.dz -com.ec -fin.ec -info.ec -med.ec -net.ec -org.ec -pro.ec -co.ee -com.ee -fie.ee -med.ee -pri.ee -com.eg -edu.eg -eun.eg -gov.eg -info.eg -name.eg -net.eg -org.eg -tv.eg -com.es -edu.es -gob.es -nom.es -org.es -biz.et -com.et -info.et -name.et -net.et -org.et -biz.fj -com.fj -info.fj -name.fj -net.fj -org.fj -pro.fj -co.fk -radio.fm -aeroport.fr -asso.fr -avocat.fr -chambagri.fr -chirurgiens-dentistes.fr -com.fr -experts-comptables.fr -geometre-expert.fr -gouv.fr -medecin.fr -nom.fr -notaires.fr -pharmacien.fr -port.fr -prd.fr -presse.fr -tm.fr -veterinaire.fr -com.ge -edu.ge -gov.ge -mil.ge -net.ge -org.ge -pvt.ge -co.gg -net.gg -org.gg -com.gh -edu.gh -org.gh -com.gi -gov.gi -ltd.gi -org.gi -co.gl -com.gl -edu.gl -net.gl -org.gl -com.gn -gov.gn -net.gn -org.gn -com.gp -mobi.gp -net.gp -org.gp -com.gr -edu.gr -net.gr -org.gr -com.gt -ind.gt -net.gt -org.gt -com.gu -co.gy -com.gy -net.gy -com.hk -edu.hk -gov.hk -idv.hk -inc.hk -ltd.hk -net.hk -org.hk -公司.hk -com.hn -edu.hn -net.hn -org.hn -com.hr -adult.ht -art.ht -asso.ht -com.ht -edu.ht -firm.ht -info.ht -net.ht -org.ht -perso.ht -pol.ht -pro.ht -rel.ht -shop.ht -2000.hu -agrar.hu -bolt.hu -casino.hu -city.hu -co.hu -erotica.hu -erotika.hu -film.hu -forum.hu -games.hu -hotel.hu -info.hu -ingatlan.hu -jogasz.hu -konyvelo.hu -lakas.hu -media.hu -news.hu -org.hu -priv.hu -reklam.hu -sex.hu -shop.hu -sport.hu -suli.hu -szex.hu -tm.hu -tozsde.hu -utazas.hu -video.hu -biz.id -co.id -my.id -or.id -web.id -co.il -net.il -org.il -ac.im -co.im -com.im -ltd.co.im -net.im -org.im -plc.co.im -co.in -firm.in -gen.in -ind.in -net.in -org.in -auz.info -com.iq -co.ir -abr.it -abruzzo.it -ag.it -agrigento.it -al.it -alessandria.it -alto-adige.it -altoadige.it -an.it -ancona.it -andria-barletta-trani.it -andria-trani-barletta.it -andriabarlettatrani.it -andriatranibarletta.it -ao.it -aosta.it -aoste.it -ap.it -aq.it -aquila.it -ar.it -arezzo.it -ascoli-piceno.it -ascolipiceno.it -asti.it -at.it -av.it -avellino.it -ba.it -balsan.it -bari.it -barletta-trani-andria.it -barlettatraniandria.it -bas.it -basilicata.it -belluno.it -benevento.it -bergamo.it -bg.it -bi.it -biella.it -bl.it -bn.it -bo.it -bologna.it -bolzano.it -bozen.it -br.it -brescia.it -brindisi.it -bs.it -bt.it -bz.it -ca.it -cagliari.it -cal.it -calabria.it -caltanissetta.it -cam.it -campania.it -campidano-medio.it -campidanomedio.it -campobasso.it -carbonia-iglesias.it -carboniaiglesias.it -carrara-massa.it -carraramassa.it -caserta.it -catania.it -catanzaro.it -cb.it -ce.it -cesena-forli.it -cesenaforli.it -ch.it -chieti.it -ci.it -cl.it -cn.it -co.it -como.it -cosenza.it -cr.it -cremona.it -crotone.it -cs.it -ct.it -cuneo.it -cz.it -dell-ogliastra.it -dellogliastra.it -emilia-romagna.it -emiliaromagna.it -emr.it -en.it -enna.it -fc.it -fe.it -fermo.it -ferrara.it -fg.it -fi.it -firenze.it -florence.it -fm.it -foggia.it -forli-cesena.it -forlicesena.it -fr.it -friuli-v-giulia.it -friuli-ve-giulia.it -friuli-vegiulia.it -friuli-venezia-giulia.it -friuli-veneziagiulia.it -friuli-vgiulia.it -friuliv-giulia.it -friulive-giulia.it -friulivegiulia.it -friulivenezia-giulia.it -friuliveneziagiulia.it -friulivgiulia.it -frosinone.it -fvg.it -ge.it -genoa.it -genova.it -go.it -gorizia.it -gr.it -grosseto.it -iglesias-carbonia.it -iglesiascarbonia.it -im.it -imperia.it -is.it -isernia.it -kr.it -la-spezia.it -laquila.it -laspezia.it -latina.it -laz.it -lazio.it -lc.it -le.it -lecce.it -lecco.it -li.it -lig.it -liguria.it -livorno.it -lo.it -lodi.it -lom.it -lombardia.it -lombardy.it -lt.it -lu.it -lucania.it -lucca.it -macerata.it -mantova.it -mar.it -marche.it -massa-carrara.it -massacarrara.it -matera.it -mb.it -mc.it -me.it -medio-campidano.it -mediocampidano.it -messina.it -mi.it -milan.it -milano.it -mn.it -mo.it -modena.it -mol.it -molise.it -monza-brianza.it -monza-e-della-brianza.it -monza.it -monzabrianza.it -monzaebrianza.it -monzaedellabrianza.it -ms.it -mt.it -na.it -naples.it -napoli.it -no.it -novara.it -nu.it -nuoro.it -og.it -ogliastra.it -olbia-tempio.it -olbiatempio.it -or.it -oristano.it -ot.it -pa.it -padova.it -padua.it -palermo.it -parma.it -pavia.it -pc.it -pd.it -pe.it -perugia.it -pesaro-urbino.it -pesarourbino.it -pescara.it -pg.it -pi.it -piacenza.it -piedmont.it -piemonte.it -pisa.it -pistoia.it -pmn.it -pn.it -po.it -pordenone.it -potenza.it -pr.it -prato.it -pt.it -pu.it -pug.it -puglia.it -pv.it -pz.it -ra.it -ragusa.it -ravenna.it -rc.it -re.it -reggio-calabria.it -reggio-emilia.it -reggiocalabria.it -reggioemilia.it -rg.it -ri.it -rieti.it -rimini.it -rm.it -rn.it -ro.it -roma.it -rome.it -rovigo.it -sa.it -salerno.it -sar.it -sardegna.it -sardinia.it -sassari.it -savona.it -si.it -sic.it -sicilia.it -sicily.it -siena.it -siracusa.it -so.it -sondrio.it -sp.it -sr.it -ss.it -suedtirol.it -sv.it -ta.it -taa.it -taranto.it -te.it -tempio-olbia.it -tempioolbia.it -teramo.it -terni.it -tn.it -to.it -torino.it -tos.it -toscana.it -tp.it -tr.it -trani-andria-barletta.it -trani-barletta-andria.it -traniandriabarletta.it -tranibarlettaandria.it -trapani.it -trentino-a-adige.it -trentino-aadige.it -trentino-alto-adige.it -trentino-altoadige.it -trentino-s-tirol.it -trentino-stirol.it -trentino-sud-tirol.it -trentino-sudtirol.it -trentino-sued-tirol.it -trentino-suedtirol.it -trentino.it -trentinoa-adige.it -trentinoaadige.it -trentinoalto-adige.it -trentinoaltoadige.it -trentinos-tirol.it -trentinosud-tirol.it -trentinosudtirol.it -trentinosued-tirol.it -trentinosuedtirol.it -trento.it -treviso.it -trieste.it -ts.it -turin.it -tuscany.it -tv.it -ud.it -udine.it -umb.it -umbria.it -urbino-pesaro.it -urbinopesaro.it -va.it -val-d-aosta.it -val-daosta.it -vald-aosta.it -valdaosta.it -valle-d-aosta.it -valle-daosta.it -valled-aosta.it -valledaosta.it -vao.it -varese.it -vb.it -vc.it -vda.it -ve.it -ven.it -veneto.it -venezia.it -venice.it -verbania.it -vercelli.it -verona.it -vi.it -vibo-valentia.it -vibovalentia.it -vicenza.it -viterbo.it -vr.it -vs.it -vt.it -vv.it -co.je -net.je -org.je -com.jm -net.jm -org.jm -com.jo -name.jo -net.jo -org.jo -sch.jo -akita.jp -co.jp -gr.jp -kyoto.jp -ne.jp -or.jp -osaka.jp -saga.jp -tokyo.jp -ac.ke -co.ke -go.ke -info.ke -me.ke -mobi.ke -ne.ke -or.ke -sc.ke -com.kg -net.kg -org.kg -com.kh -edu.kh -net.kh -org.kh -biz.ki -com.ki -edu.ki -gov.ki -info.ki -mobi.ki -net.ki -org.ki -phone.ki -tel.ki -com.km -nom.km -org.km -tm.km -com.kn -co.kr -go.kr -ms.kr -ne.kr -or.kr -pe.kr -re.kr -seoul.kr -com.kw -edu.kw -net.kw -org.kw -com.ky -net.ky -org.ky -com.kz -org.kz -com.lb -edu.lb -net.lb -org.lb -co.lc -com.lc -l.lc -net.lc -org.lc -p.lc -com.lk -edu.lk -grp.lk -hotel.lk -ltd.lk -org.lk -soc.lk -web.lk -com.lr -org.lr -co.ls -net.ls -org.ls -asn.lv -com.lv -conf.lv -edu.lv -id.lv -mil.lv -net.lv -org.lv -com.ly -id.ly -med.ly -net.ly -org.ly -plc.ly -sch.ly -ac.ma -co.ma -net.ma -org.ma -press.ma -asso.mc -tm.mc -co.mg -com.mg -mil.mg -net.mg -nom.mg -org.mg -prd.mg -tm.mg -com.mk -edu.mk -inf.mk -name.mk -net.mk -org.mk -biz.mm -com.mm -net.mm -org.mm -per.mm -com.mo -net.mo -org.mo -edu.mr -org.mr -perso.mr -co.ms -com.ms -org.ms -com.mt -net.mt -org.mt -ac.mu -co.mu -com.mu -net.mu -nom.mu -or.mu -org.mu -com.mv -ac.mw -co.mw -com.mw -coop.mw -edu.mw -int.mw -net.mw -org.mw -com.mx -net.mx -org.mx -com.my -mil.my -name.my -net.my -org.my -co.mz -edu.mz -net.mz -org.mz -alt.na -co.na -com.na -edu.na -net.na -org.na -asso.nc -nom.nc -com.ne -info.ne -int.ne -org.ne -perso.ne -auz.net -gb.net -hu.net -in.net -jp.net -ru.net -se.net -uk.net -arts.nf -com.nf -firm.nf -info.nf -net.nf -org.nf -other.nf -per.nf -rec.nf -store.nf -web.nf -com.ng -edu.ng -gov.ng -i.ng -lg.gov.ng -mobi.ng -name.ng -net.ng -org.ng -sch.ng -ac.ni -biz.ni -co.ni -com.ni -edu.ni -gob.ni -in.ni -info.ni -int.ni -mil.ni -net.ni -nom.ni -org.ni -web.ni -co.nl -com.nl -net.nl -co.no -fhs.no -folkebibl.no -fylkesbibl.no -gs.no -idrett.no -museum.no -priv.no -uenorge.no -vgs.no -aero.np -asia.np -biz.np -com.np -coop.np -info.np -mil.np -mobi.np -museum.np -name.np -net.np -org.np -pro.np -travel.np -biz.nr -com.nr -info.nr -net.nr -org.nr -co.nu -ac.nz -co.net.nz -co.nz -geek.nz -gen.nz -iwi.nz -kiwi.nz -maori.nz -net.nz -org.nz -school.nz -biz.om -co.om -com.om -med.om -mil.om -museum.om -net.om -org.om -pro.om -sch.om -ae.org -hk.org -us.org -abo.pa -com.pa -edu.pa -gob.pa -ing.pa -med.pa -net.pa -nom.pa -org.pa -sld.pa -com.pe -gob.pe -mil.pe -net.pe -nom.pe -org.pe -asso.pf -com.pf -org.pf -com.pg -net.pg -org.pg -com.ph -net.ph -org.ph -biz.pk -com.pk -net.pk -org.pk -web.pk -agro.pl -aid.pl -atm.pl -augustow.pl -auto.pl -babia-gora.pl -bedzin.pl -beskidy.pl -bialowieza.pl -bialystok.pl -bielawa.pl -bieszczady.pl -biz.pl -boleslawiec.pl -bydgoszcz.pl -bytom.pl -cieszyn.pl -com.pl -czeladz.pl -czest.pl -dlugoleka.pl -edu.pl -elblag.pl -elk.pl -glogow.pl -gmina.pl -gniezno.pl -gorlice.pl -grajewo.pl -gsm.pl -ilawa.pl -info.pl -jaworzno.pl -jelenia-gora.pl -jgora.pl -kalisz.pl -karpacz.pl -kartuzy.pl -kaszuby.pl -katowice.pl -kazimierz-dolny.pl -kepno.pl -ketrzyn.pl -klodzko.pl -kobierzyce.pl -kolobrzeg.pl -konin.pl -konskowola.pl -kutno.pl -lapy.pl -lebork.pl -legnica.pl -lezajsk.pl -limanowa.pl -lomza.pl -lowicz.pl -lubin.pl -lukow.pl -mail.pl -malbork.pl -malopolska.pl -mazowsze.pl -mazury.pl -media.pl -miasta.pl -mielec.pl -mielno.pl -mil.pl -mragowo.pl -naklo.pl -net.pl -nieruchomosci.pl -nom.pl -nowaruda.pl -nysa.pl -olawa.pl -olecko.pl -olkusz.pl -olsztyn.pl -opoczno.pl -opole.pl -org.pl -ostroda.pl -ostroleka.pl -ostrowiec.pl -ostrowwlkp.pl -pc.pl -pila.pl -pisz.pl -podhale.pl -podlasie.pl -polkowice.pl -pomorskie.pl -pomorze.pl -powiat.pl -priv.pl -prochowice.pl -pruszkow.pl -przeworsk.pl -pulawy.pl -radom.pl -rawa-maz.pl -realestate.pl -rel.pl -rybnik.pl -rzeszow.pl -sanok.pl -sejny.pl -sex.pl -shop.pl -sklep.pl -skoczow.pl -slask.pl -slupsk.pl -sos.pl -sosnowiec.pl -stalowa-wola.pl -starachowice.pl -stargard.pl -suwalki.pl -swidnica.pl -swiebodzin.pl -swinoujscie.pl -szczecin.pl -szczytno.pl -szkola.pl -targi.pl -tarnobrzeg.pl -tgory.pl -tm.pl -tourism.pl -travel.pl -turek.pl -turystyka.pl -tychy.pl -ustka.pl -walbrzych.pl -warmia.pl -warszawa.pl -waw.pl -wegrow.pl -wielun.pl -wlocl.pl -wloclawek.pl -wodzislaw.pl -wolomin.pl -wroclaw.pl -zachpomor.pl -zagan.pl -zarow.pl -zgora.pl -zgorzelec.pl -co.pl -co.pn -net.pn -org.pn -at.pr -biz.pr -ch.pr -com.pr -de.pr -eu.pr -fr.pr -info.pr -isla.pr -it.pr -name.pr -net.pr -nl.pr -org.pr -pro.pr -uk.pr -aaa.pro -aca.pro -acct.pro -arc.pro -avocat.pro -bar.pro -bus.pro -chi.pro -chiro.pro -cpa.pro -den.pro -dent.pro -eng.pro -jur.pro -law.pro -med.pro -min.pro -nur.pro -nurse.pro -pharma.pro -prof.pro -prx.pro -recht.pro -rel.pro -teach.pro -vet.pro -com.ps -net.ps -org.ps -co.pt -com.pt -org.pt -com.py -coop.py -edu.py -mil.py -net.py -org.py -com.qa -edu.qa -mil.qa -name.qa -net.qa -org.qa -sch.qa -com.re -arts.ro -co.ro -com.ro -firm.ro -info.ro -ne.ro -nom.ro -nt.ro -or.ro -org.ro -rec.ro -sa.ro -srl.ro -store.ro -tm.ro -www.ro -co.rs -edu.rs -in.rs -org.rs -adygeya.ru -bashkiria.ru -bir.ru -cbg.ru -com.ru -dagestan.ru -grozny.ru -kalmykia.ru -kustanai.ru -marine.ru -mordovia.ru -msk.ru -mytis.ru -nalchik.ru -net.ru -nov.ru -org.ru -pp.ru -pyatigorsk.ru -spb.ru -vladikavkaz.ru -vladimir.ru -ac.rw -co.rw -net.rw -org.rw -com.sa -edu.sa -med.sa -net.sa -org.sa -pub.sa -sch.sa -com.sb -net.sb -org.sb -com.sc -net.sc -org.sc -com.sd -info.sd -net.sd -com.se -com.sg -edu.sg -net.sg -org.sg -per.sg -ae.si -at.si -cn.si -co.si -de.si -uk.si -us.si -com.sl -edu.sl -net.sl -org.sl -art.sn -com.sn -edu.sn -org.sn -perso.sn -univ.sn -com.so -net.so -org.so -biz.ss -com.ss -me.ss -net.ss -abkhazia.su -adygeya.su -aktyubinsk.su -arkhangelsk.su -armenia.su -ashgabad.su -azerbaijan.su -balashov.su -bashkiria.su -bryansk.su -bukhara.su -chimkent.su -dagestan.su -east-kazakhstan.su -exnet.su -georgia.su -grozny.su -ivanovo.su -jambyl.su -kalmykia.su -kaluga.su -karacol.su -karaganda.su -karelia.su -khakassia.su -krasnodar.su -kurgan.su -kustanai.su -lenug.su -mangyshlak.su -mordovia.su -msk.su -murmansk.su -nalchik.su -navoi.su -north-kazakhstan.su -nov.su -obninsk.su -penza.su -pokrovsk.su -sochi.su -spb.su -tashkent.su -termez.su -togliatti.su -troitsk.su -tselinograd.su -tula.su -tuva.su -vladikavkaz.su -vladimir.su -vologda.su -com.sv -edu.sv -gob.sv -org.sv -com.sy -co.sz -org.sz -com.tc -net.tc -org.tc -pro.tc -com.td -net.td -org.td -tourism.td -ac.th -co.th -in.th -or.th -ac.tj -aero.tj -biz.tj -co.tj -com.tj -coop.tj -dyn.tj -go.tj -info.tj -int.tj -mil.tj -museum.tj -my.tj -name.tj -net.tj -org.tj -per.tj -pro.tj -web.tj -com.tl -net.tl -org.tl -agrinet.tn -com.tn -defense.tn -edunet.tn -ens.tn -fin.tn -ind.tn -info.tn -intl.tn -nat.tn -net.tn -org.tn -perso.tn -rnrt.tn -rns.tn -rnu.tn -tourism.tn -av.tr -bbs.tr -biz.tr -com.tr -dr.tr -gen.tr -info.tr -name.tr -net.tr -org.tr -tel.tr -tv.tr -web.tr -biz.tt -co.tt -com.tt -info.tt -jobs.tt -mobi.tt -name.tt -net.tt -org.tt -pro.tt -club.tw -com.tw -ebiz.tw -game.tw -idv.tw -net.tw -org.tw -ac.tz -co.tz -hotel.tz -info.tz -me.tz -mil.tz -mobi.tz -ne.tz -or.tz -sc.tz -tv.tz -biz.ua -cherkassy.ua -cherkasy.ua -chernigov.ua -chernivtsi.ua -chernovtsy.ua -ck.ua -cn.ua -co.ua -com.ua -crimea.ua -cv.ua -dn.ua -dnepropetrovsk.ua -dnipropetrovsk.ua -donetsk.ua -dp.ua -edu.ua -gov.ua -if.ua -in.ua -ivano-frankivsk.ua -kh.ua -kharkiv.ua -kharkov.ua -kherson.ua -khmelnitskiy.ua -kiev.ua -kirovograd.ua -km.ua -kr.ua -ks.ua -kyiv.ua -lg.ua -lt.ua -lugansk.ua -lutsk.ua -lviv.ua -mk.ua -net.ua -nikolaev.ua -od.ua -odesa.ua -odessa.ua -org.ua -pl.ua -poltava.ua -pp.ua -rivne.ua -rovno.ua -rv.ua -sebastopol.ua -sm.ua -sumy.ua -te.ua -ternopil.ua -uz.ua -uzhgorod.ua -vinnica.ua -vn.ua -volyn.ua -yalta.ua -zaporizhzhe.ua -zhitomir.ua -zp.ua -zt.ua -ac.ug -co.ug -com.ug -go.ug -ne.ug -or.ug -org.ug -sc.ug -ac.uk -barking-dagenham.sch.uk -barnet.sch.uk -barnsley.sch.uk -bathnes.sch.uk -beds.sch.uk -bexley.sch.uk -bham.sch.uk -blackburn.sch.uk -blackpool.sch.uk -bolton.sch.uk -bournemouth.sch.uk -bracknell-forest.sch.uk -bradford.sch.uk -brent.sch.uk -co.uk -doncaster.sch.uk -gov.uk -ltd.uk -me.uk -net.uk -org.uk -plc.uk -sch.uk -com.uy -edu.uy -net.uy -org.uy -biz.uz -co.uz -com.uz -net.uz -org.uz -com.vc -net.vc -org.vc -co.ve -com.ve -info.ve -net.ve -org.ve -web.ve -co.vi -com.vi -net.vi -org.vi -ac.vn -biz.vn -com.vn -edu.vn -health.vn -info.vn -int.vn -name.vn -net.vn -org.vn -pro.vn -com.vu -net.vu -org.vu -com.ws -net.ws -org.ws -com.ye -net.ye -org.ye -co.za -net.za -org.za -web.za -co.zm -com.zm -org.zm -co.zw -ком.рф -нет.рф -орг.рф -ак.срб -пр.срб -упр.срб -كمپنی.بھارت -कंपनी.भारत -কোম্পানি.ভারত -நிறுவனம்.இந்தியா -個人.香港 -公司.香港 -政府.香港 -教育.香港 -組織.香港 -網絡.香港 -gov.cn -com.ac -com.ad -com.ae -com.an -com.ao -com.aq -com.as -com.at -com.aw -com.ba -com.be -com.bf -com.bg -com.bv -com.bw -com.ca -com.cc -com.cf -com.cg -com.ch -com.ck -com.cl -com.cq -com.cr -com.cx -com.cz -com.dj -com.dk -com.eh -com.eu -com.ev -com.fi -com.fk -com.fm -com.fo -com.ga -com.gb -com.gd -com.gf -com.gm -com.gw -com.hm -com.hu -com.id -com.ie -com.il -com.in -com.io -com.ir -com.is -com.it -com.jp -com.ke -com.kp -com.kr -com.la -com.li -com.ls -com.lt -com.lu -com.ma -com.mc -com.md -com.me -com.mh -com.ml -com.mn -com.mp -com.mq -com.mr -com.mz -com.nc -com.no -com.nt -com.nu -com.nz -com.pm -com.pn -com.pw -com.rs -com.rw -com.sh -com.si -com.sj -com.sk -com.sm -com.sr -com.st -com.su -com.sz -com.tf -com.tg -com.th -com.tk -com.tm -com.to -com.tp -com.tv -com.tz -com.uk -com.us -com.va -com.vg -com.wf -com.za -com.zw -mil.cn -edu.kg -edu.cn -eu.org -us.kg -xx.kg -qzz.io -dpdns.org -ggff.net -ac.ru -edu.ru -com.ru -msk.ru -net.ru -nov.ru -org.ru -pp.ru -spb.ru -uk.co +ac.ae +co.ae +net.ae +org.ae +sch.ae +airport.aero +cargo.aero +charter.aero +com.af +edu.af +gov.af +net.af +org.af +co.ag +com.ag +net.ag +nom.ag +org.ag +com.ai +net.ai +off.ai +org.ai +com.al +net.al +org.al +co.am +com.am +net.am +north.am +org.am +radio.am +south.am +co.ao +it.ao +og.ao +pb.ao +com.ar +int.ar +net.ar +org.ar +co.at +or.at +asn.au +com.au +id.au +info.au +net.au +org.au +biz.az +co.az +com.az +edu.az +gov.az +info.az +int.az +mil.az +name.az +net.az +org.az +pp.az +pro.az +co.ba +co.bb +com.bb +net.bb +org.bb +ac.bd +com.bd +net.bd +org.bd +com.bh +co.bi +com.bi +edu.bi +info.bi +mo.bi +net.bi +or.bi +org.bi +auz.biz +com.bj +edu.bj +com.bm +net.bm +org.bm +com.bn +com.bo +net.bo +org.bo +tv.bo +abc.br +adm.br +adv.br +agr.br +am.br +aparecida.br +arq.br +art.br +ato.br +belem.br +bhz.br +bio.br +blog.br +bmd.br +boavista.br +bsb.br +campinas.br +caxias.br +cim.br +cng.br +cnt.br +com.br +coop.br +curitiba.br +ecn.br +eco.br +emp.br +eng.br +esp.br +etc.br +eti.br +far.br +flog.br +floripa.br +fm.br +fnd.br +fortal.br +fot.br +foz.br +fst.br +g12.br +ggf.br +gru.br +imb.br +ind.br +inf.br +jampa.br +jor.br +lel.br +macapa.br +maceio.br +manaus.br +mat.br +med.br +mil.br +mus.br +natal.br +net.br +nom.br +not.br +ntr.br +odo.br +org.br +palmas.br +poa.br +ppg.br +pro.br +psc.br +psi.br +qsl.br +radio.br +rec.br +recife.br +rep.br +rio.br +salvador.br +sjc.br +slg.br +srv.br +taxi.br +teo.br +tmp.br +trd.br +tur.br +tv.br +vet.br +vix.br +vlog.br +wiki.br +zlg.br +com.bs +net.bs +org.bs +com.bt +org.bt +ac.bw +co.bw +net.bw +org.bw +com.by +minsk.by +net.by +co.bz +com.bz +net.bz +org.bz +za.bz +com.cd +net.cd +org.cd +ac.ci +co.ci +com.ci +ed.ci +edu.ci +go.ci +in.ci +int.ci +net.ci +nom.ci +or.ci +org.ci +biz.ck +co.ck +edu.ck +gen.ck +gov.ck +info.ck +net.ck +org.ck +co.cm +com.cm +net.cm +ac.cn +ah.cn +bj.cn +com.cn +cq.cn +fj.cn +gd.cn +gs.cn +gx.cn +gz.cn +ha.cn +hb.cn +he.cn +hi.cn +hk.cn +hl.cn +hn.cn +jl.cn +js.cn +jx.cn +ln.cn +mo.cn +net.cn +nm.cn +nx.cn +org.cn +qh.cn +sc.cn +sd.cn +sh.cn +sn.cn +sx.cn +tj.cn +tw.cn +xj.cn +xz.cn +yn.cn +zj.cn +com.co +net.co +nom.co +ae.com +africa.com +ar.com +br.com +cn.com +co.com +de.com +eu.com +gb.com +gr.com +hk.com +hu.com +jpn.com +kr.com +mex.com +no.com +nv.com +pty-ltd.com +qc.com +ru.com +sa.com +se.com +uk.com +us.com +uy.com +za.com +it.com +co.cr +ed.cr +fi.cr +go.cr +or.cr +sa.cr +com.cu +com.cv +int.cv +net.cv +nome.cv +org.cv +publ.cv +com.cw +net.cw +ac.cy +biz.cy +com.cy +ekloges.cy +ltd.cy +name.cy +net.cy +org.cy +parliament.cy +press.cy +pro.cy +tm.cy +co.cz +co.de +com.de +biz.dk +co.dk +co.dm +com.dm +net.dm +org.dm +art.do +com.do +net.do +org.do +sld.do +web.do +com.dz +com.ec +fin.ec +info.ec +med.ec +net.ec +org.ec +pro.ec +co.ee +com.ee +fie.ee +med.ee +pri.ee +com.eg +edu.eg +eun.eg +gov.eg +info.eg +name.eg +net.eg +org.eg +tv.eg +com.es +edu.es +gob.es +nom.es +org.es +biz.et +com.et +info.et +name.et +net.et +org.et +biz.fj +com.fj +info.fj +name.fj +net.fj +org.fj +pro.fj +co.fk +radio.fm +aeroport.fr +asso.fr +avocat.fr +chambagri.fr +chirurgiens-dentistes.fr +com.fr +experts-comptables.fr +geometre-expert.fr +gouv.fr +medecin.fr +nom.fr +notaires.fr +pharmacien.fr +port.fr +prd.fr +presse.fr +tm.fr +veterinaire.fr +com.ge +edu.ge +gov.ge +mil.ge +net.ge +org.ge +pvt.ge +co.gg +net.gg +org.gg +com.gh +edu.gh +org.gh +com.gi +gov.gi +ltd.gi +org.gi +co.gl +com.gl +edu.gl +net.gl +org.gl +com.gn +gov.gn +net.gn +org.gn +com.gp +mobi.gp +net.gp +org.gp +com.gr +edu.gr +net.gr +org.gr +com.gt +ind.gt +net.gt +org.gt +com.gu +co.gy +com.gy +net.gy +com.hk +edu.hk +gov.hk +idv.hk +inc.hk +ltd.hk +net.hk +org.hk +公司.hk +com.hn +edu.hn +net.hn +org.hn +com.hr +adult.ht +art.ht +asso.ht +com.ht +edu.ht +firm.ht +info.ht +net.ht +org.ht +perso.ht +pol.ht +pro.ht +rel.ht +shop.ht +2000.hu +agrar.hu +bolt.hu +casino.hu +city.hu +co.hu +erotica.hu +erotika.hu +film.hu +forum.hu +games.hu +hotel.hu +info.hu +ingatlan.hu +jogasz.hu +konyvelo.hu +lakas.hu +media.hu +news.hu +org.hu +priv.hu +reklam.hu +sex.hu +shop.hu +sport.hu +suli.hu +szex.hu +tm.hu +tozsde.hu +utazas.hu +video.hu +biz.id +co.id +my.id +or.id +web.id +co.il +net.il +org.il +ac.im +co.im +com.im +ltd.co.im +net.im +org.im +plc.co.im +co.in +firm.in +gen.in +ind.in +net.in +org.in +auz.info +com.iq +co.ir +abr.it +abruzzo.it +ag.it +agrigento.it +al.it +alessandria.it +alto-adige.it +altoadige.it +an.it +ancona.it +andria-barletta-trani.it +andria-trani-barletta.it +andriabarlettatrani.it +andriatranibarletta.it +ao.it +aosta.it +aoste.it +ap.it +aq.it +aquila.it +ar.it +arezzo.it +ascoli-piceno.it +ascolipiceno.it +asti.it +at.it +av.it +avellino.it +ba.it +balsan.it +bari.it +barletta-trani-andria.it +barlettatraniandria.it +bas.it +basilicata.it +belluno.it +benevento.it +bergamo.it +bg.it +bi.it +biella.it +bl.it +bn.it +bo.it +bologna.it +bolzano.it +bozen.it +br.it +brescia.it +brindisi.it +bs.it +bt.it +bz.it +ca.it +cagliari.it +cal.it +calabria.it +caltanissetta.it +cam.it +campania.it +campidano-medio.it +campidanomedio.it +campobasso.it +carbonia-iglesias.it +carboniaiglesias.it +carrara-massa.it +carraramassa.it +caserta.it +catania.it +catanzaro.it +cb.it +ce.it +cesena-forli.it +cesenaforli.it +ch.it +chieti.it +ci.it +cl.it +cn.it +co.it +como.it +cosenza.it +cr.it +cremona.it +crotone.it +cs.it +ct.it +cuneo.it +cz.it +dell-ogliastra.it +dellogliastra.it +emilia-romagna.it +emiliaromagna.it +emr.it +en.it +enna.it +fc.it +fe.it +fermo.it +ferrara.it +fg.it +fi.it +firenze.it +florence.it +fm.it +foggia.it +forli-cesena.it +forlicesena.it +fr.it +friuli-v-giulia.it +friuli-ve-giulia.it +friuli-vegiulia.it +friuli-venezia-giulia.it +friuli-veneziagiulia.it +friuli-vgiulia.it +friuliv-giulia.it +friulive-giulia.it +friulivegiulia.it +friulivenezia-giulia.it +friuliveneziagiulia.it +friulivgiulia.it +frosinone.it +fvg.it +ge.it +genoa.it +genova.it +go.it +gorizia.it +gr.it +grosseto.it +iglesias-carbonia.it +iglesiascarbonia.it +im.it +imperia.it +is.it +isernia.it +kr.it +la-spezia.it +laquila.it +laspezia.it +latina.it +laz.it +lazio.it +lc.it +le.it +lecce.it +lecco.it +li.it +lig.it +liguria.it +livorno.it +lo.it +lodi.it +lom.it +lombardia.it +lombardy.it +lt.it +lu.it +lucania.it +lucca.it +macerata.it +mantova.it +mar.it +marche.it +massa-carrara.it +massacarrara.it +matera.it +mb.it +mc.it +me.it +medio-campidano.it +mediocampidano.it +messina.it +mi.it +milan.it +milano.it +mn.it +mo.it +modena.it +mol.it +molise.it +monza-brianza.it +monza-e-della-brianza.it +monza.it +monzabrianza.it +monzaebrianza.it +monzaedellabrianza.it +ms.it +mt.it +na.it +naples.it +napoli.it +no.it +novara.it +nu.it +nuoro.it +og.it +ogliastra.it +olbia-tempio.it +olbiatempio.it +or.it +oristano.it +ot.it +pa.it +padova.it +padua.it +palermo.it +parma.it +pavia.it +pc.it +pd.it +pe.it +perugia.it +pesaro-urbino.it +pesarourbino.it +pescara.it +pg.it +pi.it +piacenza.it +piedmont.it +piemonte.it +pisa.it +pistoia.it +pmn.it +pn.it +po.it +pordenone.it +potenza.it +pr.it +prato.it +pt.it +pu.it +pug.it +puglia.it +pv.it +pz.it +ra.it +ragusa.it +ravenna.it +rc.it +re.it +reggio-calabria.it +reggio-emilia.it +reggiocalabria.it +reggioemilia.it +rg.it +ri.it +rieti.it +rimini.it +rm.it +rn.it +ro.it +roma.it +rome.it +rovigo.it +sa.it +salerno.it +sar.it +sardegna.it +sardinia.it +sassari.it +savona.it +si.it +sic.it +sicilia.it +sicily.it +siena.it +siracusa.it +so.it +sondrio.it +sp.it +sr.it +ss.it +suedtirol.it +sv.it +ta.it +taa.it +taranto.it +te.it +tempio-olbia.it +tempioolbia.it +teramo.it +terni.it +tn.it +to.it +torino.it +tos.it +toscana.it +tp.it +tr.it +trani-andria-barletta.it +trani-barletta-andria.it +traniandriabarletta.it +tranibarlettaandria.it +trapani.it +trentino-a-adige.it +trentino-aadige.it +trentino-alto-adige.it +trentino-altoadige.it +trentino-s-tirol.it +trentino-stirol.it +trentino-sud-tirol.it +trentino-sudtirol.it +trentino-sued-tirol.it +trentino-suedtirol.it +trentino.it +trentinoa-adige.it +trentinoaadige.it +trentinoalto-adige.it +trentinoaltoadige.it +trentinos-tirol.it +trentinosud-tirol.it +trentinosudtirol.it +trentinosued-tirol.it +trentinosuedtirol.it +trento.it +treviso.it +trieste.it +ts.it +turin.it +tuscany.it +tv.it +ud.it +udine.it +umb.it +umbria.it +urbino-pesaro.it +urbinopesaro.it +va.it +val-d-aosta.it +val-daosta.it +vald-aosta.it +valdaosta.it +valle-d-aosta.it +valle-daosta.it +valled-aosta.it +valledaosta.it +vao.it +varese.it +vb.it +vc.it +vda.it +ve.it +ven.it +veneto.it +venezia.it +venice.it +verbania.it +vercelli.it +verona.it +vi.it +vibo-valentia.it +vibovalentia.it +vicenza.it +viterbo.it +vr.it +vs.it +vt.it +vv.it +co.je +net.je +org.je +com.jm +net.jm +org.jm +com.jo +name.jo +net.jo +org.jo +sch.jo +akita.jp +co.jp +gr.jp +kyoto.jp +ne.jp +or.jp +osaka.jp +saga.jp +tokyo.jp +ac.ke +co.ke +go.ke +info.ke +me.ke +mobi.ke +ne.ke +or.ke +sc.ke +com.kg +net.kg +org.kg +com.kh +edu.kh +net.kh +org.kh +biz.ki +com.ki +edu.ki +gov.ki +info.ki +mobi.ki +net.ki +org.ki +phone.ki +tel.ki +com.km +nom.km +org.km +tm.km +com.kn +co.kr +go.kr +ms.kr +ne.kr +or.kr +pe.kr +re.kr +seoul.kr +com.kw +edu.kw +net.kw +org.kw +com.ky +net.ky +org.ky +com.kz +org.kz +com.lb +edu.lb +net.lb +org.lb +co.lc +com.lc +l.lc +net.lc +org.lc +p.lc +com.lk +edu.lk +grp.lk +hotel.lk +ltd.lk +org.lk +soc.lk +web.lk +com.lr +org.lr +co.ls +net.ls +org.ls +asn.lv +com.lv +conf.lv +edu.lv +id.lv +mil.lv +net.lv +org.lv +com.ly +id.ly +med.ly +net.ly +org.ly +plc.ly +sch.ly +ac.ma +co.ma +net.ma +org.ma +press.ma +asso.mc +tm.mc +co.mg +com.mg +mil.mg +net.mg +nom.mg +org.mg +prd.mg +tm.mg +com.mk +edu.mk +inf.mk +name.mk +net.mk +org.mk +biz.mm +com.mm +net.mm +org.mm +per.mm +com.mo +net.mo +org.mo +edu.mr +org.mr +perso.mr +co.ms +com.ms +org.ms +com.mt +net.mt +org.mt +ac.mu +co.mu +com.mu +net.mu +nom.mu +or.mu +org.mu +com.mv +ac.mw +co.mw +com.mw +coop.mw +edu.mw +int.mw +net.mw +org.mw +com.mx +net.mx +org.mx +com.my +mil.my +name.my +net.my +org.my +co.mz +edu.mz +net.mz +org.mz +alt.na +co.na +com.na +edu.na +net.na +org.na +asso.nc +nom.nc +com.ne +info.ne +int.ne +org.ne +perso.ne +auz.net +gb.net +hu.net +in.net +jp.net +ru.net +se.net +uk.net +arts.nf +com.nf +firm.nf +info.nf +net.nf +org.nf +other.nf +per.nf +rec.nf +store.nf +web.nf +com.ng +edu.ng +gov.ng +i.ng +lg.gov.ng +mobi.ng +name.ng +net.ng +org.ng +sch.ng +ac.ni +biz.ni +co.ni +com.ni +edu.ni +gob.ni +in.ni +info.ni +int.ni +mil.ni +net.ni +nom.ni +org.ni +web.ni +co.nl +com.nl +net.nl +co.no +fhs.no +folkebibl.no +fylkesbibl.no +gs.no +idrett.no +museum.no +priv.no +uenorge.no +vgs.no +aero.np +asia.np +biz.np +com.np +coop.np +info.np +mil.np +mobi.np +museum.np +name.np +net.np +org.np +pro.np +travel.np +biz.nr +com.nr +info.nr +net.nr +org.nr +co.nu +ac.nz +co.net.nz +co.nz +geek.nz +gen.nz +iwi.nz +kiwi.nz +maori.nz +net.nz +org.nz +school.nz +biz.om +co.om +com.om +med.om +mil.om +museum.om +net.om +org.om +pro.om +sch.om +ae.org +hk.org +us.org +abo.pa +com.pa +edu.pa +gob.pa +ing.pa +med.pa +net.pa +nom.pa +org.pa +sld.pa +com.pe +gob.pe +mil.pe +net.pe +nom.pe +org.pe +asso.pf +com.pf +org.pf +com.pg +net.pg +org.pg +com.ph +net.ph +org.ph +biz.pk +com.pk +net.pk +org.pk +web.pk +agro.pl +aid.pl +atm.pl +augustow.pl +auto.pl +babia-gora.pl +bedzin.pl +beskidy.pl +bialowieza.pl +bialystok.pl +bielawa.pl +bieszczady.pl +biz.pl +boleslawiec.pl +bydgoszcz.pl +bytom.pl +cieszyn.pl +com.pl +czeladz.pl +czest.pl +dlugoleka.pl +edu.pl +elblag.pl +elk.pl +glogow.pl +gmina.pl +gniezno.pl +gorlice.pl +grajewo.pl +gsm.pl +ilawa.pl +info.pl +jaworzno.pl +jelenia-gora.pl +jgora.pl +kalisz.pl +karpacz.pl +kartuzy.pl +kaszuby.pl +katowice.pl +kazimierz-dolny.pl +kepno.pl +ketrzyn.pl +klodzko.pl +kobierzyce.pl +kolobrzeg.pl +konin.pl +konskowola.pl +kutno.pl +lapy.pl +lebork.pl +legnica.pl +lezajsk.pl +limanowa.pl +lomza.pl +lowicz.pl +lubin.pl +lukow.pl +mail.pl +malbork.pl +malopolska.pl +mazowsze.pl +mazury.pl +media.pl +miasta.pl +mielec.pl +mielno.pl +mil.pl +mragowo.pl +naklo.pl +net.pl +nieruchomosci.pl +nom.pl +nowaruda.pl +nysa.pl +olawa.pl +olecko.pl +olkusz.pl +olsztyn.pl +opoczno.pl +opole.pl +org.pl +ostroda.pl +ostroleka.pl +ostrowiec.pl +ostrowwlkp.pl +pc.pl +pila.pl +pisz.pl +podhale.pl +podlasie.pl +polkowice.pl +pomorskie.pl +pomorze.pl +powiat.pl +priv.pl +prochowice.pl +pruszkow.pl +przeworsk.pl +pulawy.pl +radom.pl +rawa-maz.pl +realestate.pl +rel.pl +rybnik.pl +rzeszow.pl +sanok.pl +sejny.pl +sex.pl +shop.pl +sklep.pl +skoczow.pl +slask.pl +slupsk.pl +sos.pl +sosnowiec.pl +stalowa-wola.pl +starachowice.pl +stargard.pl +suwalki.pl +swidnica.pl +swiebodzin.pl +swinoujscie.pl +szczecin.pl +szczytno.pl +szkola.pl +targi.pl +tarnobrzeg.pl +tgory.pl +tm.pl +tourism.pl +travel.pl +turek.pl +turystyka.pl +tychy.pl +ustka.pl +walbrzych.pl +warmia.pl +warszawa.pl +waw.pl +wegrow.pl +wielun.pl +wlocl.pl +wloclawek.pl +wodzislaw.pl +wolomin.pl +wroclaw.pl +zachpomor.pl +zagan.pl +zarow.pl +zgora.pl +zgorzelec.pl +co.pl +co.pn +net.pn +org.pn +at.pr +biz.pr +ch.pr +com.pr +de.pr +eu.pr +fr.pr +info.pr +isla.pr +it.pr +name.pr +net.pr +nl.pr +org.pr +pro.pr +uk.pr +aaa.pro +aca.pro +acct.pro +arc.pro +avocat.pro +bar.pro +bus.pro +chi.pro +chiro.pro +cpa.pro +den.pro +dent.pro +eng.pro +jur.pro +law.pro +med.pro +min.pro +nur.pro +nurse.pro +pharma.pro +prof.pro +prx.pro +recht.pro +rel.pro +teach.pro +vet.pro +com.ps +net.ps +org.ps +co.pt +com.pt +org.pt +com.py +coop.py +edu.py +mil.py +net.py +org.py +com.qa +edu.qa +mil.qa +name.qa +net.qa +org.qa +sch.qa +com.re +arts.ro +co.ro +com.ro +firm.ro +info.ro +ne.ro +nom.ro +nt.ro +or.ro +org.ro +rec.ro +sa.ro +srl.ro +store.ro +tm.ro +www.ro +co.rs +edu.rs +in.rs +org.rs +adygeya.ru +bashkiria.ru +bir.ru +cbg.ru +com.ru +dagestan.ru +grozny.ru +kalmykia.ru +kustanai.ru +marine.ru +mordovia.ru +msk.ru +mytis.ru +nalchik.ru +net.ru +nov.ru +org.ru +pp.ru +pyatigorsk.ru +spb.ru +vladikavkaz.ru +vladimir.ru +ac.rw +co.rw +net.rw +org.rw +com.sa +edu.sa +med.sa +net.sa +org.sa +pub.sa +sch.sa +com.sb +net.sb +org.sb +com.sc +net.sc +org.sc +com.sd +info.sd +net.sd +com.se +com.sg +edu.sg +net.sg +org.sg +per.sg +ae.si +at.si +cn.si +co.si +de.si +uk.si +us.si +com.sl +edu.sl +net.sl +org.sl +art.sn +com.sn +edu.sn +org.sn +perso.sn +univ.sn +com.so +net.so +org.so +biz.ss +com.ss +me.ss +net.ss +abkhazia.su +adygeya.su +aktyubinsk.su +arkhangelsk.su +armenia.su +ashgabad.su +azerbaijan.su +balashov.su +bashkiria.su +bryansk.su +bukhara.su +chimkent.su +dagestan.su +east-kazakhstan.su +exnet.su +georgia.su +grozny.su +ivanovo.su +jambyl.su +kalmykia.su +kaluga.su +karacol.su +karaganda.su +karelia.su +khakassia.su +krasnodar.su +kurgan.su +kustanai.su +lenug.su +mangyshlak.su +mordovia.su +msk.su +murmansk.su +nalchik.su +navoi.su +north-kazakhstan.su +nov.su +obninsk.su +penza.su +pokrovsk.su +sochi.su +spb.su +tashkent.su +termez.su +togliatti.su +troitsk.su +tselinograd.su +tula.su +tuva.su +vladikavkaz.su +vladimir.su +vologda.su +com.sv +edu.sv +gob.sv +org.sv +com.sy +co.sz +org.sz +com.tc +net.tc +org.tc +pro.tc +com.td +net.td +org.td +tourism.td +ac.th +co.th +in.th +or.th +ac.tj +aero.tj +biz.tj +co.tj +com.tj +coop.tj +dyn.tj +go.tj +info.tj +int.tj +mil.tj +museum.tj +my.tj +name.tj +net.tj +org.tj +per.tj +pro.tj +web.tj +com.tl +net.tl +org.tl +agrinet.tn +com.tn +defense.tn +edunet.tn +ens.tn +fin.tn +ind.tn +info.tn +intl.tn +nat.tn +net.tn +org.tn +perso.tn +rnrt.tn +rns.tn +rnu.tn +tourism.tn +av.tr +bbs.tr +biz.tr +com.tr +dr.tr +gen.tr +info.tr +name.tr +net.tr +org.tr +tel.tr +tv.tr +web.tr +biz.tt +co.tt +com.tt +info.tt +jobs.tt +mobi.tt +name.tt +net.tt +org.tt +pro.tt +club.tw +com.tw +ebiz.tw +game.tw +idv.tw +net.tw +org.tw +ac.tz +co.tz +hotel.tz +info.tz +me.tz +mil.tz +mobi.tz +ne.tz +or.tz +sc.tz +tv.tz +biz.ua +cherkassy.ua +cherkasy.ua +chernigov.ua +chernivtsi.ua +chernovtsy.ua +ck.ua +cn.ua +co.ua +com.ua +crimea.ua +cv.ua +dn.ua +dnepropetrovsk.ua +dnipropetrovsk.ua +donetsk.ua +dp.ua +edu.ua +gov.ua +if.ua +in.ua +ivano-frankivsk.ua +kh.ua +kharkiv.ua +kharkov.ua +kherson.ua +khmelnitskiy.ua +kiev.ua +kirovograd.ua +km.ua +kr.ua +ks.ua +kyiv.ua +lg.ua +lt.ua +lugansk.ua +lutsk.ua +lviv.ua +mk.ua +net.ua +nikolaev.ua +od.ua +odesa.ua +odessa.ua +org.ua +pl.ua +poltava.ua +pp.ua +rivne.ua +rovno.ua +rv.ua +sebastopol.ua +sm.ua +sumy.ua +te.ua +ternopil.ua +uz.ua +uzhgorod.ua +vinnica.ua +vn.ua +volyn.ua +yalta.ua +zaporizhzhe.ua +zhitomir.ua +zp.ua +zt.ua +ac.ug +co.ug +com.ug +go.ug +ne.ug +or.ug +org.ug +sc.ug +ac.uk +barking-dagenham.sch.uk +barnet.sch.uk +barnsley.sch.uk +bathnes.sch.uk +beds.sch.uk +bexley.sch.uk +bham.sch.uk +blackburn.sch.uk +blackpool.sch.uk +bolton.sch.uk +bournemouth.sch.uk +bracknell-forest.sch.uk +bradford.sch.uk +brent.sch.uk +co.uk +doncaster.sch.uk +gov.uk +ltd.uk +me.uk +net.uk +org.uk +plc.uk +sch.uk +com.uy +edu.uy +net.uy +org.uy +biz.uz +co.uz +com.uz +net.uz +org.uz +com.vc +net.vc +org.vc +co.ve +com.ve +info.ve +net.ve +org.ve +web.ve +co.vi +com.vi +net.vi +org.vi +ac.vn +biz.vn +com.vn +edu.vn +health.vn +info.vn +int.vn +name.vn +net.vn +org.vn +pro.vn +com.vu +net.vu +org.vu +com.ws +net.ws +org.ws +com.ye +net.ye +org.ye +co.za +net.za +org.za +web.za +co.zm +com.zm +org.zm +co.zw +ком.рф +нет.рф +орг.рф +ак.срб +пр.срб +упр.срб +كمپنی.بھارت +कंपनी.भारत +কোম্পানি.ভারত +நிறுவனம்.இந்தியா +個人.香港 +公司.香港 +政府.香港 +教育.香港 +組織.香港 +網絡.香港 +gov.cn +com.ac +com.ad +com.ae +com.an +com.ao +com.aq +com.as +com.at +com.aw +com.ba +com.be +com.bf +com.bg +com.bv +com.bw +com.ca +com.cc +com.cf +com.cg +com.ch +com.ck +com.cl +com.cq +com.cr +com.cx +com.cz +com.dj +com.dk +com.eh +com.eu +com.ev +com.fi +com.fk +com.fm +com.fo +com.ga +com.gb +com.gd +com.gf +com.gm +com.gw +com.hm +com.hu +com.id +com.ie +com.il +com.in +com.io +com.ir +com.is +com.it +com.jp +com.ke +com.kp +com.kr +com.la +com.li +com.ls +com.lt +com.lu +com.ma +com.mc +com.md +com.me +com.mh +com.ml +com.mn +com.mp +com.mq +com.mr +com.mz +com.nc +com.no +com.nt +com.nu +com.nz +com.pm +com.pn +com.pw +com.rs +com.rw +com.sh +com.si +com.sj +com.sk +com.sm +com.sr +com.st +com.su +com.sz +com.tf +com.tg +com.th +com.tk +com.tm +com.to +com.tp +com.tv +com.tz +com.uk +com.us +com.va +com.vg +com.wf +com.za +com.zw +mil.cn +edu.kg +edu.cn +eu.org +us.kg +xx.kg +qzz.io +dpdns.org +ggff.net +ac.ru +edu.ru +com.ru +msk.ru +net.ru +nov.ru +org.ru +pp.ru +spb.ru +uk.co gov.scot \ No newline at end of file diff --git a/app/lib/CertInterface.php b/app/lib/CertInterface.php index 00346c2..a3ac3ee 100644 --- a/app/lib/CertInterface.php +++ b/app/lib/CertInterface.php @@ -1,24 +1,24 @@ - [ - 'name' => '阿里云', - 'config' => [ - 'ak' => 'AccessKeyId', - 'sk' => 'AccessKeySecret', - ], - 'remark' => 1, //是否支持备注,1单独设置备注,2和记录一起设置 - 'status' => true, //是否支持启用暂停 - 'redirect' => true, //是否支持域名转发 - 'log' => true, //是否支持查看日志 - 'weight' => false, //是否支持权重 - 'page' => false, //是否客户端分页 - 'add' => true, //是否支持添加域名 - ], - 'dnspod' => [ - 'name' => '腾讯云', - 'config' => [ - 'ak' => 'SecretId', - 'sk' => 'SecretKey', - ], - 'remark' => 1, - 'status' => true, - 'redirect' => true, - 'log' => true, - 'weight' => true, - 'page' => false, - 'add' => true, - ], - 'huawei' => [ - 'name' => '华为云', - 'config' => [ - 'ak' => 'AccessKeyId', - 'sk' => 'SecretAccessKey', - ], - 'remark' => 2, - 'status' => true, - 'redirect' => false, - 'log' => false, - 'weight' => true, - 'page' => false, - 'add' => true, - ], - 'baidu' => [ - 'name' => '百度云', - 'config' => [ - 'ak' => 'AccessKey', - 'sk' => 'SecretKey', - ], - 'remark' => 2, - 'status' => false, - 'redirect' => false, - 'log' => false, - 'weight' => false, - 'page' => true, - 'add' => true, - ], - 'west' => [ - 'name' => '西部数码', - 'config' => [ - 'ak' => '用户名', - 'sk' => 'API密码', - ], - 'remark' => 0, - 'status' => true, - 'redirect' => false, - 'log' => false, - 'weight' => false, - 'page' => false, - 'add' => false, - ], - 'huoshan' => [ - 'name' => '火山引擎', - 'config' => [ - 'ak' => 'AccessKeyId', - 'sk' => 'SecretAccessKey', - ], - 'remark' => 2, - 'status' => true, - 'redirect' => false, - 'log' => false, - 'weight' => true, - 'page' => false, - 'add' => true, - ], - 'jdcloud' => [ - 'name' => '京东云', - 'config' => [ - 'ak' => 'AccessKeyId', - 'sk' => 'AccessKeySecret', - ], - 'remark' => 0, - 'status' => true, - 'redirect' => true, - 'log' => false, - 'weight' => true, - 'page' => false, - 'add' => true, - ], - 'dnsla' => [ - 'name' => 'DNSLA', - 'config' => [ - 'ak' => 'APIID', - 'sk' => 'API密钥', - ], - 'remark' => 0, - 'status' => true, - 'redirect' => true, - 'log' => false, - 'weight' => true, - 'page' => false, - 'add' => true, - ], - 'cloudflare' => [ - 'name' => 'Cloudflare', - 'config' => [ - 'ak' => '邮箱地址', - 'sk' => 'API密钥/令牌', - ], - 'remark' => 2, - 'status' => true, - 'redirect' => false, - 'log' => false, - 'weight' => false, - 'page' => false, - 'add' => true, - ], - 'namesilo' => [ - 'name' => 'NameSilo', - 'config' => [ - 'ak' => '账户名', - 'sk' => 'API Key', - ], - 'remark' => 0, - 'status' => false, - 'redirect' => false, - 'log' => false, - 'weight' => false, - 'page' => true, - 'add' => false, - ], - 'powerdns' => [ - 'name' => 'PowerDNS', - 'config' => [ - 'ak' => 'IP地址', - 'sk' => '端口', - 'ext' => 'API KEY', - ], - 'remark' => 2, - 'status' => true, - 'redirect' => false, - 'log' => false, - 'weight' => false, - 'page' => true, - 'add' => true, - ], - ]; - - public static $line_name = [ - 'aliyun' => ['DEF' => 'default', 'CT' => 'telecom', 'CU' => 'unicom', 'CM' => 'mobile', 'AB' => 'oversea'], - 'dnspod' => ['DEF' => '0', 'CT' => '10=0', 'CU' => '10=1', 'CM' => '10=3', 'AB' => '3=0'], - 'huawei' => ['DEF' => 'default_view', 'CT' => 'Dianxin', 'CU' => 'Liantong', 'CM' => 'Yidong', 'AB' => 'Abroad'], - 'west' => ['DEF' => '', 'CT' => 'LTEL', 'CU' => 'LCNC', 'CM' => 'LMOB', 'AB' => 'LFOR'], - 'dnsla' => ['DEF' => '', 'CT' => '84613316902921216', 'CU' => '84613316923892736', 'CM' => '84613316953252864', 'AB' => ''], - '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'], - 'cloudflare' => ['DEF' => '0'], - 'namesilo' => ['DEF' => 'default'], - 'powerdns' => ['DEF' => 'default'], - ]; - - public static function getList() - { - return self::$dns_config; - } - - private static function getConfig($aid) - { - $account = Db::name('account')->where('id', $aid)->find(); - if (!$account) return false; - return $account; - } - - /** - * @return DnsInterface|bool - */ - public static function getModel($aid, $domain = null, $domainid = null) - { - $config = self::getConfig($aid); - if (!$config) return false; - $dnstype = $config['type']; - $class = "\\app\\lib\\dns\\{$dnstype}"; - if (class_exists($class)) { - $config['domain'] = $domain; - $config['domainid'] = $domainid; - $model = new $class($config); - return $model; - } - return false; - } - - /** - * @return DnsInterface|bool - */ - public static function getModel2($config) - { - $dnstype = $config['type']; - $class = "\\app\\lib\\dns\\{$dnstype}"; - if (class_exists($class)) { - $config['domain'] = $config['name']; - $config['domainid'] = $config['thirdid']; - $model = new $class($config); - return $model; - } - return false; - } -} + [ + 'name' => '阿里云', + 'config' => [ + 'ak' => 'AccessKeyId', + 'sk' => 'AccessKeySecret', + ], + 'remark' => 1, //是否支持备注,1单独设置备注,2和记录一起设置 + 'status' => true, //是否支持启用暂停 + 'redirect' => true, //是否支持域名转发 + 'log' => true, //是否支持查看日志 + 'weight' => false, //是否支持权重 + 'page' => false, //是否客户端分页 + 'add' => true, //是否支持添加域名 + ], + 'dnspod' => [ + 'name' => '腾讯云', + 'config' => [ + 'ak' => 'SecretId', + 'sk' => 'SecretKey', + ], + 'remark' => 1, + 'status' => true, + 'redirect' => true, + 'log' => true, + 'weight' => true, + 'page' => false, + 'add' => true, + ], + 'huawei' => [ + 'name' => '华为云', + 'config' => [ + 'ak' => 'AccessKeyId', + 'sk' => 'SecretAccessKey', + ], + 'remark' => 2, + 'status' => true, + 'redirect' => false, + 'log' => false, + 'weight' => true, + 'page' => false, + 'add' => true, + ], + 'baidu' => [ + 'name' => '百度云', + 'config' => [ + 'ak' => 'AccessKey', + 'sk' => 'SecretKey', + ], + 'remark' => 2, + 'status' => false, + 'redirect' => false, + 'log' => false, + 'weight' => false, + 'page' => true, + 'add' => true, + ], + 'west' => [ + 'name' => '西部数码', + 'config' => [ + 'ak' => '用户名', + 'sk' => 'API密码', + ], + 'remark' => 0, + 'status' => true, + 'redirect' => false, + 'log' => false, + 'weight' => false, + 'page' => false, + 'add' => false, + ], + 'huoshan' => [ + 'name' => '火山引擎', + 'config' => [ + 'ak' => 'AccessKeyId', + 'sk' => 'SecretAccessKey', + ], + 'remark' => 2, + 'status' => true, + 'redirect' => false, + 'log' => false, + 'weight' => true, + 'page' => false, + 'add' => true, + ], + 'jdcloud' => [ + 'name' => '京东云', + 'config' => [ + 'ak' => 'AccessKeyId', + 'sk' => 'AccessKeySecret', + ], + 'remark' => 0, + 'status' => true, + 'redirect' => true, + 'log' => false, + 'weight' => true, + 'page' => false, + 'add' => true, + ], + 'dnsla' => [ + 'name' => 'DNSLA', + 'config' => [ + 'ak' => 'APIID', + 'sk' => 'API密钥', + ], + 'remark' => 0, + 'status' => true, + 'redirect' => true, + 'log' => false, + 'weight' => true, + 'page' => false, + 'add' => true, + ], + 'cloudflare' => [ + 'name' => 'Cloudflare', + 'config' => [ + 'ak' => '邮箱地址', + 'sk' => 'API密钥/令牌', + ], + 'remark' => 2, + 'status' => true, + 'redirect' => false, + 'log' => false, + 'weight' => false, + 'page' => false, + 'add' => true, + ], + 'namesilo' => [ + 'name' => 'NameSilo', + 'config' => [ + 'ak' => '账户名', + 'sk' => 'API Key', + ], + 'remark' => 0, + 'status' => false, + 'redirect' => false, + 'log' => false, + 'weight' => false, + 'page' => true, + 'add' => false, + ], + 'powerdns' => [ + 'name' => 'PowerDNS', + 'config' => [ + 'ak' => 'IP地址', + 'sk' => '端口', + 'ext' => 'API KEY', + ], + 'remark' => 2, + 'status' => true, + 'redirect' => false, + 'log' => false, + 'weight' => false, + 'page' => true, + 'add' => true, + ], + ]; + + public static $line_name = [ + 'aliyun' => ['DEF' => 'default', 'CT' => 'telecom', 'CU' => 'unicom', 'CM' => 'mobile', 'AB' => 'oversea'], + 'dnspod' => ['DEF' => '0', 'CT' => '10=0', 'CU' => '10=1', 'CM' => '10=3', 'AB' => '3=0'], + 'huawei' => ['DEF' => 'default_view', 'CT' => 'Dianxin', 'CU' => 'Liantong', 'CM' => 'Yidong', 'AB' => 'Abroad'], + 'west' => ['DEF' => '', 'CT' => 'LTEL', 'CU' => 'LCNC', 'CM' => 'LMOB', 'AB' => 'LFOR'], + 'dnsla' => ['DEF' => '', 'CT' => '84613316902921216', 'CU' => '84613316923892736', 'CM' => '84613316953252864', 'AB' => ''], + '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'], + 'cloudflare' => ['DEF' => '0'], + 'namesilo' => ['DEF' => 'default'], + 'powerdns' => ['DEF' => 'default'], + ]; + + public static function getList() + { + return self::$dns_config; + } + + private static function getConfig($aid) + { + $account = Db::name('account')->where('id', $aid)->find(); + if (!$account) return false; + return $account; + } + + /** + * @return DnsInterface|bool + */ + public static function getModel($aid, $domain = null, $domainid = null) + { + $config = self::getConfig($aid); + if (!$config) return false; + $dnstype = $config['type']; + $class = "\\app\\lib\\dns\\{$dnstype}"; + if (class_exists($class)) { + $config['domain'] = $domain; + $config['domainid'] = $domainid; + $model = new $class($config); + return $model; + } + return false; + } + + /** + * @return DnsInterface|bool + */ + public static function getModel2($config) + { + $dnstype = $config['type']; + $class = "\\app\\lib\\dns\\{$dnstype}"; + if (class_exists($class)) { + $config['domain'] = $config['name']; + $config['domainid'] = $config['thirdid']; + $model = new $class($config); + return $model; + } + return false; + } +} diff --git a/app/lib/DnsInterface.php b/app/lib/DnsInterface.php index 270aa11..d5f5eaa 100644 --- a/app/lib/DnsInterface.php +++ b/app/lib/DnsInterface.php @@ -1,36 +1,36 @@ -generateSecret(); - } - $this->secret = $secret; - } - - public static function create(?string $secret = null) - { - return new self($secret); - } - - public function getSecret(): string - { - return $this->secret; - } - - public function getLabel(): ?string - { - return $this->label; - } - - public function setLabel(string $label): void - { - $this->label = $label; - } - - public function getIssuer(): ?string - { - return $this->issuer; - } - - public function setIssuer(string $issuer): void - { - $this->issuer = $issuer; - } - - public function verify(string $otp, ?int $timestamp = null, ?int $window = null): bool - { - $timestamp = $this->getTimestamp($timestamp); - - if (null === $window) { - return $this->compareOTP($this->at($timestamp), $otp); - } - - return $this->verifyOtpWithWindow($otp, $timestamp, $window); - } - - private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): bool - { - $window = abs($window); - - for ($i = 0; $i <= $window; ++$i) { - $next = $i * $this->period + $timestamp; - $previous = -$i * $this->period + $timestamp; - $valid = $this->compareOTP($this->at($next), $otp) || - $this->compareOTP($this->at($previous), $otp); - - if ($valid) { - return true; - } - } - - return false; - } - - public function getProvisioningUri(): string - { - $params = []; - if (30 !== $this->period) { - $params['period'] = $this->period; - } - if (0 !== $this->epoch) { - $params['epoch'] = $this->epoch; - } - $label = $this->getLabel(); - if (null === $label) { - throw new \InvalidArgumentException('The label is not set.'); - } - if ($this->hasColon($label)) { - throw new \InvalidArgumentException('Label must not contain a colon.'); - } - $params['issuer'] = $this->getIssuer(); - $params['secret'] = $this->getSecret(); - $query = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($params)); - return sprintf('otpauth://totp/%s?%s', rawurlencode((null !== $this->getIssuer() ? $this->getIssuer() . ':' : '') . $label), $query); - } - - /** - * The OTP at the specified input. - */ - private function generateOTP(int $input): string - { - $hash = hash_hmac($this->digest, $this->intToByteString($input), $this->base32_decode($this->getSecret()), true); - - $hmac = array_values(unpack('C*', $hash)); - - $offset = ($hmac[\count($hmac) - 1] & 0xF); - $code = ($hmac[$offset + 0] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF); - $otp = $code % (10 ** $this->digits); - - return str_pad((string) $otp, $this->digits, '0', STR_PAD_LEFT); - } - - private function at(int $timestamp): string - { - return $this->generateOTP($this->timecode($timestamp)); - } - - private function timecode(int $timestamp): int - { - return (int) floor(($timestamp - $this->epoch) / $this->period); - } - - private function getTimestamp(?int $timestamp): int - { - $timestamp = $timestamp ?? time(); - if ($timestamp < 0) { - throw new \InvalidArgumentException('Timestamp must be at least 0.'); - } - - return $timestamp; - } - - private function generateSecret(): string - { - return strtoupper($this->base32_encode(random_bytes(20))); - } - - private function base32_encode($data) - { - $dataSize = strlen($data); - $res = ''; - $remainder = 0; - $remainderSize = 0; - - for ($i = 0; $i < $dataSize; $i++) { - $b = ord($data[$i]); - $remainder = ($remainder << 8) | $b; - $remainderSize += 8; - while ($remainderSize > 4) { - $remainderSize -= 5; - $c = $remainder & (31 << $remainderSize); - $c >>= $remainderSize; - $res .= self::$BASE32_ALPHABET[$c]; - } - } - if ($remainderSize > 0) { - $remainder <<= (5 - $remainderSize); - $c = $remainder & 31; - $res .= self::$BASE32_ALPHABET[$c]; - } - - return $res; - } - - private function base32_decode($data) - { - $data = strtolower($data); - $dataSize = strlen($data); - $buf = 0; - $bufSize = 0; - $res = ''; - - for ($i = 0; $i < $dataSize; $i++) { - $c = $data[$i]; - $b = strpos(self::$BASE32_ALPHABET, $c); - if ($b === false) { - throw new \Exception('Encoded string is invalid, it contains unknown char #'.ord($c)); - } - $buf = ($buf << 5) | $b; - $bufSize += 5; - if ($bufSize > 7) { - $bufSize -= 8; - $b = ($buf & (0xff << $bufSize)) >> $bufSize; - $res .= chr($b); - } - } - - return $res; - } - - private function intToByteString(int $int): string - { - $result = []; - while (0 !== $int) { - $result[] = \chr($int & 0xFF); - $int >>= 8; - } - - return str_pad(implode(array_reverse($result)), 8, "\000", STR_PAD_LEFT); - } - - private function compareOTP(string $safe, string $user): bool - { - return hash_equals($safe, $user); - } - - private function hasColon(string $value): bool - { - $colons = [':', '%3A', '%3a']; - foreach ($colons as $colon) { - if (false !== mb_strpos($value, $colon)) { - return true; - } - } - - return false; - } -} +generateSecret(); + } + $this->secret = $secret; + } + + public static function create(?string $secret = null) + { + return new self($secret); + } + + public function getSecret(): string + { + return $this->secret; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): void + { + $this->label = $label; + } + + public function getIssuer(): ?string + { + return $this->issuer; + } + + public function setIssuer(string $issuer): void + { + $this->issuer = $issuer; + } + + public function verify(string $otp, ?int $timestamp = null, ?int $window = null): bool + { + $timestamp = $this->getTimestamp($timestamp); + + if (null === $window) { + return $this->compareOTP($this->at($timestamp), $otp); + } + + return $this->verifyOtpWithWindow($otp, $timestamp, $window); + } + + private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): bool + { + $window = abs($window); + + for ($i = 0; $i <= $window; ++$i) { + $next = $i * $this->period + $timestamp; + $previous = -$i * $this->period + $timestamp; + $valid = $this->compareOTP($this->at($next), $otp) || + $this->compareOTP($this->at($previous), $otp); + + if ($valid) { + return true; + } + } + + return false; + } + + public function getProvisioningUri(): string + { + $params = []; + if (30 !== $this->period) { + $params['period'] = $this->period; + } + if (0 !== $this->epoch) { + $params['epoch'] = $this->epoch; + } + $label = $this->getLabel(); + if (null === $label) { + throw new \InvalidArgumentException('The label is not set.'); + } + if ($this->hasColon($label)) { + throw new \InvalidArgumentException('Label must not contain a colon.'); + } + $params['issuer'] = $this->getIssuer(); + $params['secret'] = $this->getSecret(); + $query = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($params)); + return sprintf('otpauth://totp/%s?%s', rawurlencode((null !== $this->getIssuer() ? $this->getIssuer() . ':' : '') . $label), $query); + } + + /** + * The OTP at the specified input. + */ + private function generateOTP(int $input): string + { + $hash = hash_hmac($this->digest, $this->intToByteString($input), $this->base32_decode($this->getSecret()), true); + + $hmac = array_values(unpack('C*', $hash)); + + $offset = ($hmac[\count($hmac) - 1] & 0xF); + $code = ($hmac[$offset + 0] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF); + $otp = $code % (10 ** $this->digits); + + return str_pad((string) $otp, $this->digits, '0', STR_PAD_LEFT); + } + + private function at(int $timestamp): string + { + return $this->generateOTP($this->timecode($timestamp)); + } + + private function timecode(int $timestamp): int + { + return (int) floor(($timestamp - $this->epoch) / $this->period); + } + + private function getTimestamp(?int $timestamp): int + { + $timestamp = $timestamp ?? time(); + if ($timestamp < 0) { + throw new \InvalidArgumentException('Timestamp must be at least 0.'); + } + + return $timestamp; + } + + private function generateSecret(): string + { + return strtoupper($this->base32_encode(random_bytes(20))); + } + + private function base32_encode($data) + { + $dataSize = strlen($data); + $res = ''; + $remainder = 0; + $remainderSize = 0; + + for ($i = 0; $i < $dataSize; $i++) { + $b = ord($data[$i]); + $remainder = ($remainder << 8) | $b; + $remainderSize += 8; + while ($remainderSize > 4) { + $remainderSize -= 5; + $c = $remainder & (31 << $remainderSize); + $c >>= $remainderSize; + $res .= self::$BASE32_ALPHABET[$c]; + } + } + if ($remainderSize > 0) { + $remainder <<= (5 - $remainderSize); + $c = $remainder & 31; + $res .= self::$BASE32_ALPHABET[$c]; + } + + return $res; + } + + private function base32_decode($data) + { + $data = strtolower($data); + $dataSize = strlen($data); + $buf = 0; + $bufSize = 0; + $res = ''; + + for ($i = 0; $i < $dataSize; $i++) { + $c = $data[$i]; + $b = strpos(self::$BASE32_ALPHABET, $c); + if ($b === false) { + throw new \Exception('Encoded string is invalid, it contains unknown char #'.ord($c)); + } + $buf = ($buf << 5) | $b; + $bufSize += 5; + if ($bufSize > 7) { + $bufSize -= 8; + $b = ($buf & (0xff << $bufSize)) >> $bufSize; + $res .= chr($b); + } + } + + return $res; + } + + private function intToByteString(int $int): string + { + $result = []; + while (0 !== $int) { + $result[] = \chr($int & 0xFF); + $int >>= 8; + } + + return str_pad(implode(array_reverse($result)), 8, "\000", STR_PAD_LEFT); + } + + private function compareOTP(string $safe, string $user): bool + { + return hash_equals($safe, $user); + } + + private function hasColon(string $value): bool + { + $colons = [':', '%3A', '%3a']; + foreach ($colons as $colon) { + if (false !== mb_strpos($value, $colon)) { + return true; + } + } + + return false; + } +} diff --git a/app/lib/cert/aliyun.php b/app/lib/cert/aliyun.php index 4cd0bc9..2611d4a 100644 --- a/app/lib/cert/aliyun.php +++ b/app/lib/cert/aliyun.php @@ -1,168 +1,168 @@ -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->config = $config; - } - - public function register() - { - if (empty($this->AccessKeyId) || empty($this->AccessKeySecret) || empty($this->config['username']) || empty($this->config['phone']) || empty($this->config['email'])) throw new Exception('必填参数不能为空'); - $param = ['Action' => 'ListUserCertificateOrder']; - $this->request($param, true); - return true; - } - - public function buyCert($domainList, &$order) - { - $param = ['Action' => 'DescribePackageState', 'ProductCode' => 'digicert-free-1-free']; - $data = $this->request($param, true); - if (!isset($data['TotalCount']) || $data['TotalCount'] == 0) throw new Exception('没有可用的免费证书资源包'); - $this->log('证书资源包总数量:' . $data['TotalCount'] . ',已使用数量:' . $data['UsedCount']); - } - - public function createOrder($domainList, &$order, $keytype, $keysize) - { - if (empty($domainList)) throw new Exception('域名列表不能为空'); - $domain = $domainList[0]; - $param = [ - 'Action' => 'CreateCertificateRequest', - 'ProductCode' => 'digicert-free-1-free', - 'Username' => $this->config['username'], - 'Phone' => $this->config['phone'], - 'Email' => $this->config['email'], - 'Domain' => $domain, - 'ValidateType' => 'DNS' - ]; - $data = $this->request($param, true); - if (empty($data['OrderId'])) throw new Exception('证书申请失败,OrderId为空'); - $order['OrderId'] = $data['OrderId']; - - sleep(3); - - $param = [ - 'Action' => 'DescribeCertificateState', - 'OrderId' => $order['OrderId'], - ]; - $data = $this->request($param, true); - - $dnsList = []; - if ($data['Type'] == 'domain_verify') { - $mainDomain = getMainDomain($domain); - $name = substr($data['RecordDomain'], 0, -(strlen($mainDomain) + 1)); - $dnsList[$mainDomain][] = ['name' => $name, 'type' => $data['RecordType'], 'value' => $data['RecordValue']]; - } - - return $dnsList; - } - - public function authOrder($domainList, $order) {} - - public function getAuthStatus($domainList, $order) - { - $param = [ - 'Action' => 'DescribeCertificateState', - 'OrderId' => $order['OrderId'], - ]; - $data = $this->request($param, true); - if ($data['Type'] == 'certificate') { - return true; - } elseif ($data['Type'] == 'verify_fail') { - throw new Exception('证书审核失败'); - } else { - return false; - } - } - - public function finalizeOrder($domainList, $order, $keytype, $keysize) - { - $param = [ - 'Action' => 'DescribeCertificateState', - 'OrderId' => $order['OrderId'], - ]; - $data = $this->request($param, true); - $fullchain = $data['Certificate']; - $private_key = $data['PrivateKey']; - if (empty($fullchain) || empty($private_key)) throw new Exception('证书内容获取失败'); - - $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) - { - $param = [ - 'Action' => 'CancelCertificateForPackageRequest', - 'OrderId' => $order['OrderId'], - ]; - $this->request($param); - } - - public function cancel($order) - { - $param = [ - 'Action' => 'DescribeCertificateState', - 'OrderId' => $order['OrderId'], - ]; - $data = $this->request($param, true); - if ($data['Type'] == 'domain_verify' || $data['Type'] == 'process') { - $param = [ - 'Action' => 'CancelOrderRequest', - 'OrderId' => $order['OrderId'], - ]; - $this->request($param); - usleep(500000); - } - if ($data['Type'] == 'domain_verify' || $data['Type'] == 'process' || $data['Type'] == 'payed' || $data['Type'] == 'verify_fail') { - $param = [ - 'Action' => 'DeleteCertificateRequest', - 'OrderId' => $order['OrderId'], - ]; - $this->request($param); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - private function request($param, $returnData = false) - { - $this->log('Request:' . json_encode($param, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); - $result = $this->client->request($param); - $response = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!strpos($response, '"Type":"certificate"')) { - $this->log('Response:' . $response); - } - return $returnData ? $result : true; - } -} +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->config = $config; + } + + public function register() + { + if (empty($this->AccessKeyId) || empty($this->AccessKeySecret) || empty($this->config['username']) || empty($this->config['phone']) || empty($this->config['email'])) throw new Exception('必填参数不能为空'); + $param = ['Action' => 'ListUserCertificateOrder']; + $this->request($param, true); + return true; + } + + public function buyCert($domainList, &$order) + { + $param = ['Action' => 'DescribePackageState', 'ProductCode' => 'digicert-free-1-free']; + $data = $this->request($param, true); + if (!isset($data['TotalCount']) || $data['TotalCount'] == 0) throw new Exception('没有可用的免费证书资源包'); + $this->log('证书资源包总数量:' . $data['TotalCount'] . ',已使用数量:' . $data['UsedCount']); + } + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + $domain = $domainList[0]; + $param = [ + 'Action' => 'CreateCertificateRequest', + 'ProductCode' => 'digicert-free-1-free', + 'Username' => $this->config['username'], + 'Phone' => $this->config['phone'], + 'Email' => $this->config['email'], + 'Domain' => $domain, + 'ValidateType' => 'DNS' + ]; + $data = $this->request($param, true); + if (empty($data['OrderId'])) throw new Exception('证书申请失败,OrderId为空'); + $order['OrderId'] = $data['OrderId']; + + sleep(3); + + $param = [ + 'Action' => 'DescribeCertificateState', + 'OrderId' => $order['OrderId'], + ]; + $data = $this->request($param, true); + + $dnsList = []; + if ($data['Type'] == 'domain_verify') { + $mainDomain = getMainDomain($domain); + $name = substr($data['RecordDomain'], 0, -(strlen($mainDomain) + 1)); + $dnsList[$mainDomain][] = ['name' => $name, 'type' => $data['RecordType'], 'value' => $data['RecordValue']]; + } + + return $dnsList; + } + + public function authOrder($domainList, $order) {} + + public function getAuthStatus($domainList, $order) + { + $param = [ + 'Action' => 'DescribeCertificateState', + 'OrderId' => $order['OrderId'], + ]; + $data = $this->request($param, true); + if ($data['Type'] == 'certificate') { + return true; + } elseif ($data['Type'] == 'verify_fail') { + throw new Exception('证书审核失败'); + } else { + return false; + } + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + $param = [ + 'Action' => 'DescribeCertificateState', + 'OrderId' => $order['OrderId'], + ]; + $data = $this->request($param, true); + $fullchain = $data['Certificate']; + $private_key = $data['PrivateKey']; + if (empty($fullchain) || empty($private_key)) throw new Exception('证书内容获取失败'); + + $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) + { + $param = [ + 'Action' => 'CancelCertificateForPackageRequest', + 'OrderId' => $order['OrderId'], + ]; + $this->request($param); + } + + public function cancel($order) + { + $param = [ + 'Action' => 'DescribeCertificateState', + 'OrderId' => $order['OrderId'], + ]; + $data = $this->request($param, true); + if ($data['Type'] == 'domain_verify' || $data['Type'] == 'process') { + $param = [ + 'Action' => 'CancelOrderRequest', + 'OrderId' => $order['OrderId'], + ]; + $this->request($param); + usleep(500000); + } + if ($data['Type'] == 'domain_verify' || $data['Type'] == 'process' || $data['Type'] == 'payed' || $data['Type'] == 'verify_fail') { + $param = [ + 'Action' => 'DeleteCertificateRequest', + 'OrderId' => $order['OrderId'], + ]; + $this->request($param); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($param, $returnData = false) + { + $this->log('Request:' . json_encode($param, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + $result = $this->client->request($param); + $response = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if (!strpos($response, '"Type":"certificate"')) { + $this->log('Response:' . $response); + } + return $returnData ? $result : true; + } +} diff --git a/app/lib/cert/customacme.php b/app/lib/cert/customacme.php index 24dde90..1467167 100644 --- a/app/lib/cert/customacme.php +++ b/app/lib/cert/customacme.php @@ -1,114 +1,114 @@ -config = $config; - $this->ac = new ACMECert($config['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['directory'])) throw new Exception('ACME地址不能为空'); - if (empty($this->config['email'])) throw new Exception('邮件地址不能为空'); - - if (!empty($this->ext['key'])) { - if (!empty($this->config['kid']) && !empty($this->config['key'])) { - $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); - } else { - $kid = $this->ac->register(true, $this->config['email']); - } - return ['kid' => $kid, 'key' => $this->ext['key']]; - } - - $key = $this->ac->generateRSAKey(2048); - $this->ac->loadAccountKey($key); - if (!empty($this->config['kid']) && !empty($this->config['key'])) { - $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); - } else { - $kid = $this->ac->register(true, $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'])) { - foreach ($order['challenges'] as $opts) { - $mainDomain = getMainDomain($opts['domain']); - $name = substr($opts['key'], 0, -(strlen($mainDomain) + 1)); - $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; - } - } - - 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); - } -} +config = $config; + $this->ac = new ACMECert($config['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['directory'])) throw new Exception('ACME地址不能为空'); + if (empty($this->config['email'])) throw new Exception('邮件地址不能为空'); + + if (!empty($this->ext['key'])) { + if (!empty($this->config['kid']) && !empty($this->config['key'])) { + $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); + } else { + $kid = $this->ac->register(true, $this->config['email']); + } + return ['kid' => $kid, 'key' => $this->ext['key']]; + } + + $key = $this->ac->generateRSAKey(2048); + $this->ac->loadAccountKey($key); + if (!empty($this->config['kid']) && !empty($this->config['key'])) { + $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); + } else { + $kid = $this->ac->register(true, $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'])) { + foreach ($order['challenges'] as $opts) { + $mainDomain = getMainDomain($opts['domain']); + $name = substr($opts['key'], 0, -(strlen($mainDomain) + 1)); + $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; + } + } + + 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); + } +} diff --git a/app/lib/cert/huoshan.php b/app/lib/cert/huoshan.php index f469292..cb61968 100644 --- a/app/lib/cert/huoshan.php +++ b/app/lib/cert/huoshan.php @@ -1,164 +1,164 @@ -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); - } - - public function register() - { - if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); - $this->request('GET', 'CertificateGetInstance', ['limit'=>1,'offset'=>0]); - return true; - } - - public function buyCert($domainList, &$order) - { - $data = $this->request('GET', 'CertificateGetOrganization'); - if(empty($data['content'])) throw new Exception('请先添加信息模板'); - $order['organization_id'] = $data['content'][0]['id']; - } - - public function createOrder($domainList, &$order, $keytype, $keysize) - { - if (empty($domainList)) throw new Exception('域名列表不能为空'); - $domain = $domainList[0]; - $param = [ - 'plan' => 'digicert_free_standard_dv', - 'common_name' => $domain, - 'organization_id' => $order['organization_id'], - 'key_alg' => strtolower($keytype), - 'validation_type' => 'dns_txt', - ]; - $instance_id = $this->request('POST', 'QuickApplyCertificate', $param); - if(empty($instance_id)) throw new Exception('证书申请失败,证书实例ID为空'); - $order['instance_id'] = $instance_id; - - sleep(3); - - $param = [ - 'instance_id' => $instance_id, - ]; - $data = $this->request('GET', 'CertificateGetDcvParam', $param); - - $dnsList = []; - if (!empty($data['domains_to_be_validated'])) { - $type = $data['validation_type'] == 'dns_cname' ? 'CNAME' : 'TXT'; - foreach ($data['domains_to_be_validated'] as $opts) { - $mainDomain = getMainDomain($domain); - $name = substr($opts['validation_domain'], 0, -(strlen($mainDomain) + 1)); - $dnsList[$mainDomain][] = ['name' => $name, 'type' => $type, 'value' => $opts['value']]; - } - } - return $dnsList; - } - - public function authOrder($domainList, $order) - { - $query = [ - 'instance_id' => $order['instance_id'], - ]; - $param = [ - 'action' => '', - ]; - $this->request('POST', 'CertificateProgressInstanceOrder', $param, $query); - } - - public function getAuthStatus($domainList, $order) - { - $param = [ - 'instance_id' => $order['instance_id'], - ]; - $data = $this->request('GET', 'CertificateGetInstance', $param); - if(empty($data['content'])) throw new Exception('证书信息获取失败'); - $data = $data['content'][0]; - if($data['order_status'] == 300 && $data['certificate_exist'] == 1){ - return true; - }elseif($data['order_status'] == 302){ - throw new Exception('证书申请失败'); - }else{ - return false; - } - } - - public function finalizeOrder($domainList, $order, $keytype, $keysize) - { - $param = [ - 'instance_id' => $order['instance_id'], - ]; - $data = $this->request('GET', 'CertificateGetInstance', $param); - if (empty($data['content'])) throw new Exception('证书信息获取失败'); - $data = $data['content'][0]; - if (!isset($data['ssl']['certificate']['chain'])) throw new Exception('证书内容获取失败'); - - $fullchain = implode('', $data['ssl']['certificate']['chain']); - $private_key = $data['ssl']['certificate']['private_key']; - - return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $data['issuer'], 'subject' => $data['common_name']['CN'], 'validFrom' => intval($data['certificate_not_before_ms']/1000), 'validTo' => intval($data['certificate_not_after_ms']/1000)]; - } - - public function revoke($order, $pem) - { - $query = [ - 'instance_id' => $order['instance_id'], - ]; - $param = [ - 'action' => 'revoke', - 'reason' => '关联域名错误', - ]; - $this->request('POST', 'CertificateProgressInstanceOrder', $param, $query); - } - - public function cancel($order) - { - $query = [ - 'instance_id' => $order['instance_id'], - ]; - $param = [ - 'action' => 'cancel', - ]; - $this->request('POST', 'CertificateProgressInstanceOrder', $param, $query); - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - private function request($method, $action, $params = [], $query = []) - { - $this->log('Action:'.$action.PHP_EOL.'Request:'.json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); - $result = $this->client->request($method, $action, $params, $query); - if (is_array($result)) { - $this->log('Response:'.json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); - } - return $result; - } -} +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); + } + + public function register() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $this->request('GET', 'CertificateGetInstance', ['limit'=>1,'offset'=>0]); + return true; + } + + public function buyCert($domainList, &$order) + { + $data = $this->request('GET', 'CertificateGetOrganization'); + if(empty($data['content'])) throw new Exception('请先添加信息模板'); + $order['organization_id'] = $data['content'][0]['id']; + } + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + $domain = $domainList[0]; + $param = [ + 'plan' => 'digicert_free_standard_dv', + 'common_name' => $domain, + 'organization_id' => $order['organization_id'], + 'key_alg' => strtolower($keytype), + 'validation_type' => 'dns_txt', + ]; + $instance_id = $this->request('POST', 'QuickApplyCertificate', $param); + if(empty($instance_id)) throw new Exception('证书申请失败,证书实例ID为空'); + $order['instance_id'] = $instance_id; + + sleep(3); + + $param = [ + 'instance_id' => $instance_id, + ]; + $data = $this->request('GET', 'CertificateGetDcvParam', $param); + + $dnsList = []; + if (!empty($data['domains_to_be_validated'])) { + $type = $data['validation_type'] == 'dns_cname' ? 'CNAME' : 'TXT'; + foreach ($data['domains_to_be_validated'] as $opts) { + $mainDomain = getMainDomain($domain); + $name = substr($opts['validation_domain'], 0, -(strlen($mainDomain) + 1)); + $dnsList[$mainDomain][] = ['name' => $name, 'type' => $type, 'value' => $opts['value']]; + } + } + return $dnsList; + } + + public function authOrder($domainList, $order) + { + $query = [ + 'instance_id' => $order['instance_id'], + ]; + $param = [ + 'action' => '', + ]; + $this->request('POST', 'CertificateProgressInstanceOrder', $param, $query); + } + + public function getAuthStatus($domainList, $order) + { + $param = [ + 'instance_id' => $order['instance_id'], + ]; + $data = $this->request('GET', 'CertificateGetInstance', $param); + if(empty($data['content'])) throw new Exception('证书信息获取失败'); + $data = $data['content'][0]; + if($data['order_status'] == 300 && $data['certificate_exist'] == 1){ + return true; + }elseif($data['order_status'] == 302){ + throw new Exception('证书申请失败'); + }else{ + return false; + } + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + $param = [ + 'instance_id' => $order['instance_id'], + ]; + $data = $this->request('GET', 'CertificateGetInstance', $param); + if (empty($data['content'])) throw new Exception('证书信息获取失败'); + $data = $data['content'][0]; + if (!isset($data['ssl']['certificate']['chain'])) throw new Exception('证书内容获取失败'); + + $fullchain = implode('', $data['ssl']['certificate']['chain']); + $private_key = $data['ssl']['certificate']['private_key']; + + return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $data['issuer'], 'subject' => $data['common_name']['CN'], 'validFrom' => intval($data['certificate_not_before_ms']/1000), 'validTo' => intval($data['certificate_not_after_ms']/1000)]; + } + + public function revoke($order, $pem) + { + $query = [ + 'instance_id' => $order['instance_id'], + ]; + $param = [ + 'action' => 'revoke', + 'reason' => '关联域名错误', + ]; + $this->request('POST', 'CertificateProgressInstanceOrder', $param, $query); + } + + public function cancel($order) + { + $query = [ + 'instance_id' => $order['instance_id'], + ]; + $param = [ + 'action' => 'cancel', + ]; + $this->request('POST', 'CertificateProgressInstanceOrder', $param, $query); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($method, $action, $params = [], $query = []) + { + $this->log('Action:'.$action.PHP_EOL.'Request:'.json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + $result = $this->client->request($method, $action, $params, $query); + if (is_array($result)) { + $this->log('Response:'.json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + } + return $result; + } +} diff --git a/app/lib/cert/letsencrypt.php b/app/lib/cert/letsencrypt.php index e7a9d36..5eb05f5 100644 --- a/app/lib/cert/letsencrypt.php +++ b/app/lib/cert/letsencrypt.php @@ -1,113 +1,113 @@ - 'https://acme-v02.api.letsencrypt.org/directory', - 'staging' => 'https://acme-staging-v02.api.letsencrypt.org/directory' - ); - private $ac; - private $config; - private $ext; - - public function __construct($config, $ext = null) - { - $this->config = $config; - if (empty($config['mode'])) $config['mode'] = 'live'; - $this->ac = new ACMECert($this->directories[$config['mode']], (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->ext['key'])) { - $kid = $this->ac->register(true, $this->config['email']); - return ['kid' => $kid, 'key' => $this->ext['key']]; - } - - $key = $this->ac->generateRSAKey(2048); - $this->ac->loadAccountKey($key); - $kid = $this->ac->register(true, $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'])) { - foreach ($order['challenges'] as $opts) { - $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 "letsencrypt.org"']; - }*/ - $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; - } - } - - 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); - } -} + 'https://acme-v02.api.letsencrypt.org/directory', + 'staging' => 'https://acme-staging-v02.api.letsencrypt.org/directory' + ); + private $ac; + private $config; + private $ext; + + public function __construct($config, $ext = null) + { + $this->config = $config; + if (empty($config['mode'])) $config['mode'] = 'live'; + $this->ac = new ACMECert($this->directories[$config['mode']], (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->ext['key'])) { + $kid = $this->ac->register(true, $this->config['email']); + return ['kid' => $kid, 'key' => $this->ext['key']]; + } + + $key = $this->ac->generateRSAKey(2048); + $this->ac->loadAccountKey($key); + $kid = $this->ac->register(true, $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'])) { + foreach ($order['challenges'] as $opts) { + $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 "letsencrypt.org"']; + }*/ + $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; + } + } + + 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); + } +} diff --git a/app/lib/cert/tencent.php b/app/lib/cert/tencent.php index 9a578cd..1ac01a0 100644 --- a/app/lib/cert/tencent.php +++ b/app/lib/cert/tencent.php @@ -1,200 +1,200 @@ -SecretId = $config['SecretId']; - $this->SecretKey = $config['SecretKey']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - $this->client = new TencentCloud($this->SecretId, $this->SecretKey, $this->endpoint, $this->service, $this->version, null, $this->proxy); - $this->email = $config['email']; - } - - public function register() - { - if (empty($this->SecretId) || empty($this->SecretKey) || empty($this->email)) throw new Exception('必填参数不能为空'); - $this->request('DescribeCertificates', []); - return true; - } - - public function buyCert($domainList, &$order) {} - - public function createOrder($domainList, &$order, $keytype, $keysize) - { - if (empty($domainList)) throw new Exception('域名列表不能为空'); - $domain = $domainList[0]; - $param = [ - 'DvAuthMethod' => 'DNS', - 'DomainName' => $domain, - 'ContactEmail' => $this->email, - 'CsrEncryptAlgo' => $keytype, - 'CsrKeyParameter' => $keytype == 'ECC' ? 'prime256v1' : '2048', - ]; - $data = $this->request('ApplyCertificate', $param); - if (empty($data['CertificateId'])) throw new Exception('证书申请失败,CertificateId为空'); - $order['CertificateId'] = $data['CertificateId']; - - $param = [ - 'CertificateId' => $order['CertificateId'], - ]; - $data = $this->request('DescribeCertificate', $param); - $order['OrderId'] = $data['OrderId']; - - $dnsList = []; - if (!empty($data['DvAuthDetail']['DvAuths'])) { - foreach ($data['DvAuthDetail']['DvAuths'] as $opts) { - $mainDomain = getMainDomain($opts['DvAuthKey']); - $name = substr($opts['DvAuthKey'], 0, -(strlen($mainDomain) + 1)); - $dnsList[$mainDomain][] = ['name' => $name, 'type' => $opts['DvAuthVerifyType'] ?? 'CNAME', 'value' => $opts['DvAuthValue']]; - } - } - - return $dnsList; - } - - public function authOrder($domainList, $order) - { - $param = [ - 'CertificateId' => $order['CertificateId'], - ]; - $data = $this->request('DescribeCertificate', $param); - if ($data['Status'] == 0 || $data['Status'] == 4) { - $this->request('CompleteCertificate', $param); - sleep(3); - } - } - - public function getAuthStatus($domainList, $order) - { - $param = [ - 'CertificateId' => $order['CertificateId'], - ]; - $data = $this->request('DescribeCertificate', $param); - if ($data['Status'] == 1) { - return true; - } elseif ($data['Status'] == 2) { - throw new Exception('证书审核失败' . (empty($data['StatusMsg'] ? '' : ':' . $data['StatusMsg']))); - } else { - return false; - } - } - - public function finalizeOrder($domainList, $order, $keytype, $keysize) - { - $param = [ - 'CertificateIds' => [$order['CertificateId']], - 'SwitchStatus' => 1, - ]; - $this->request('ModifyCertificatesExpiringNotificationSwitch', $param); - - if (!is_dir(app()->getRuntimePath() . 'cert')) mkdir(app()->getRuntimePath() . 'cert'); - $param = [ - 'CertificateId' => $order['CertificateId'], - 'ServiceType' => 'nginx', - ]; - $data = $this->request('DescribeDownloadCertificateUrl', $param); - $file_data = http_request($data['DownloadCertificateUrl'], null, null, null, null, $this->proxy); - $file_data = $file_data['body'] ?? null; - if (empty($file_data)) throw new Exception('下载证书失败'); - $file_path = app()->getRuntimePath() . 'cert/' . $data['DownloadFilename']; - $file_name = substr($data['DownloadFilename'], 0, -4); - file_put_contents($file_path, $file_data); - - $zip = new \ZipArchive; - if ($zip->open($file_path) === true) { - $zip->extractTo(app()->getRuntimePath() . 'cert/'); - $zip->close(); - } else { - throw new Exception('解压证书失败'); - } - $cert_dir = app()->getRuntimePath() . 'cert/' . $file_name; - - $items = scandir($cert_dir); - if ($items === false) throw new Exception('解压后的证书文件夹不存在'); - $private_key = null; - $fullchain = null; - foreach ($items as $item) { - if (substr($item, -4) == '.key') { - $private_key = file_get_contents($cert_dir . '/' . $item); - } elseif (substr($item, -4) == '.crt') { - $fullchain = file_get_contents($cert_dir . '/' . $item); - } - } - if (empty($private_key) || empty($fullchain)) throw new Exception('解压后的证书文件夹内未找到证书文件'); - - clearDirectory($cert_dir); - rmdir($cert_dir); - unlink($file_path); - - $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) - { - $param = [ - 'CertificateId' => $order['CertificateId'], - ]; - $action = 'RevokeCertificate'; - $data = $this->request($action, $param); - - if (!empty($data['RevokeDomainValidateAuths'])) { - $dnsList = []; - foreach ($data['RevokeDomainValidateAuths'] as $opts) { - $mainDomain = getMainDomain($opts['DomainValidateAuthKey']); - $name = substr($opts['DomainValidateAuthKey'], 0, -(strlen($mainDomain) + 1)); - $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['DomainValidateAuthValue']]; - } - \app\utils\CertDnsUtils::addDns($dnsList, function ($txt) { - $this->log($txt); - }); - } - } - - public function cancel($order) - { - $param = [ - 'CertificateId' => $order['CertificateId'], - ]; - $action = 'CancelAuditCertificate'; - $this->request($action, $param); - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - private function request($action, $param) - { - $this->log('Action:' . $action . PHP_EOL . 'Request:' . json_encode($param, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); - $result = $this->client->request($action, $param); - $this->log('Response:' . json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); - return $result; - } -} +SecretId = $config['SecretId']; + $this->SecretKey = $config['SecretKey']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, $this->endpoint, $this->service, $this->version, null, $this->proxy); + $this->email = $config['email']; + } + + public function register() + { + if (empty($this->SecretId) || empty($this->SecretKey) || empty($this->email)) throw new Exception('必填参数不能为空'); + $this->request('DescribeCertificates', []); + return true; + } + + public function buyCert($domainList, &$order) {} + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + $domain = $domainList[0]; + $param = [ + 'DvAuthMethod' => 'DNS', + 'DomainName' => $domain, + 'ContactEmail' => $this->email, + 'CsrEncryptAlgo' => $keytype, + 'CsrKeyParameter' => $keytype == 'ECC' ? 'prime256v1' : '2048', + ]; + $data = $this->request('ApplyCertificate', $param); + if (empty($data['CertificateId'])) throw new Exception('证书申请失败,CertificateId为空'); + $order['CertificateId'] = $data['CertificateId']; + + $param = [ + 'CertificateId' => $order['CertificateId'], + ]; + $data = $this->request('DescribeCertificate', $param); + $order['OrderId'] = $data['OrderId']; + + $dnsList = []; + if (!empty($data['DvAuthDetail']['DvAuths'])) { + foreach ($data['DvAuthDetail']['DvAuths'] as $opts) { + $mainDomain = getMainDomain($opts['DvAuthKey']); + $name = substr($opts['DvAuthKey'], 0, -(strlen($mainDomain) + 1)); + $dnsList[$mainDomain][] = ['name' => $name, 'type' => $opts['DvAuthVerifyType'] ?? 'CNAME', 'value' => $opts['DvAuthValue']]; + } + } + + return $dnsList; + } + + public function authOrder($domainList, $order) + { + $param = [ + 'CertificateId' => $order['CertificateId'], + ]; + $data = $this->request('DescribeCertificate', $param); + if ($data['Status'] == 0 || $data['Status'] == 4) { + $this->request('CompleteCertificate', $param); + sleep(3); + } + } + + public function getAuthStatus($domainList, $order) + { + $param = [ + 'CertificateId' => $order['CertificateId'], + ]; + $data = $this->request('DescribeCertificate', $param); + if ($data['Status'] == 1) { + return true; + } elseif ($data['Status'] == 2) { + throw new Exception('证书审核失败' . (empty($data['StatusMsg'] ? '' : ':' . $data['StatusMsg']))); + } else { + return false; + } + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + $param = [ + 'CertificateIds' => [$order['CertificateId']], + 'SwitchStatus' => 1, + ]; + $this->request('ModifyCertificatesExpiringNotificationSwitch', $param); + + if (!is_dir(app()->getRuntimePath() . 'cert')) mkdir(app()->getRuntimePath() . 'cert'); + $param = [ + 'CertificateId' => $order['CertificateId'], + 'ServiceType' => 'nginx', + ]; + $data = $this->request('DescribeDownloadCertificateUrl', $param); + $file_data = http_request($data['DownloadCertificateUrl'], null, null, null, null, $this->proxy); + $file_data = $file_data['body'] ?? null; + if (empty($file_data)) throw new Exception('下载证书失败'); + $file_path = app()->getRuntimePath() . 'cert/' . $data['DownloadFilename']; + $file_name = substr($data['DownloadFilename'], 0, -4); + file_put_contents($file_path, $file_data); + + $zip = new \ZipArchive; + if ($zip->open($file_path) === true) { + $zip->extractTo(app()->getRuntimePath() . 'cert/'); + $zip->close(); + } else { + throw new Exception('解压证书失败'); + } + $cert_dir = app()->getRuntimePath() . 'cert/' . $file_name; + + $items = scandir($cert_dir); + if ($items === false) throw new Exception('解压后的证书文件夹不存在'); + $private_key = null; + $fullchain = null; + foreach ($items as $item) { + if (substr($item, -4) == '.key') { + $private_key = file_get_contents($cert_dir . '/' . $item); + } elseif (substr($item, -4) == '.crt') { + $fullchain = file_get_contents($cert_dir . '/' . $item); + } + } + if (empty($private_key) || empty($fullchain)) throw new Exception('解压后的证书文件夹内未找到证书文件'); + + clearDirectory($cert_dir); + rmdir($cert_dir); + unlink($file_path); + + $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) + { + $param = [ + 'CertificateId' => $order['CertificateId'], + ]; + $action = 'RevokeCertificate'; + $data = $this->request($action, $param); + + if (!empty($data['RevokeDomainValidateAuths'])) { + $dnsList = []; + foreach ($data['RevokeDomainValidateAuths'] as $opts) { + $mainDomain = getMainDomain($opts['DomainValidateAuthKey']); + $name = substr($opts['DomainValidateAuthKey'], 0, -(strlen($mainDomain) + 1)); + $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['DomainValidateAuthValue']]; + } + \app\utils\CertDnsUtils::addDns($dnsList, function ($txt) { + $this->log($txt); + }); + } + } + + public function cancel($order) + { + $param = [ + 'CertificateId' => $order['CertificateId'], + ]; + $action = 'CancelAuditCertificate'; + $this->request($action, $param); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($action, $param) + { + $this->log('Action:' . $action . PHP_EOL . 'Request:' . json_encode($param, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + $result = $this->client->request($action, $param); + $this->log('Response:' . json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return $result; + } +} diff --git a/app/lib/cert/ucloud.php b/app/lib/cert/ucloud.php index 70db5e0..ea075c7 100644 --- a/app/lib/cert/ucloud.php +++ b/app/lib/cert/ucloud.php @@ -1,188 +1,188 @@ -PublicKey = $config['PublicKey']; - $this->PrivateKey = $config['PrivateKey']; - $this->client = new UcloudClient($this->PublicKey, $this->PrivateKey); - $this->config = $config; - } - - public function register() - { - if (empty($this->PublicKey) || empty($this->PrivateKey) || empty($this->config['username']) || empty($this->config['phone']) || empty($this->config['email'])) throw new Exception('必填参数不能为空'); - $param = ['Mode' => 'free']; - $this->request('GetCertificateList', $param); - return true; - } - - public function buyCert($domainList, &$order) - { - $param = [ - 'CertificateBrand' => 'TrustAsia', - 'CertificateName' => 'TrustAsiaC1DVFree', - 'DomainsCount' => 1, - 'ValidYear' => 1, - ]; - $data = $this->request('PurchaseCertificate', $param); - if (!isset($data['CertificateID'])) throw new Exception('证书购买失败,CertificateID为空'); - $order['CertificateID'] = $data['CertificateID']; - } - - public function createOrder($domainList, &$order, $keytype, $keysize) - { - if (empty($domainList)) throw new Exception('域名列表不能为空'); - $domain = $domainList[0]; - $param = [ - 'CertificateID' => $order['CertificateID'], - 'Domains' => $domain, - 'CSROnline' => 1, - 'CSREncryptAlgo' => ['RSA' => 'RSA', 'ECC' => 'ECDSA'][$keytype], - 'CSRKeyParameter' => ['2048' => '2048', '3072' => '3072', '256' => 'prime256v1', '384' => 'prime384v1'][$keysize], - 'CompanyName' => '公司名称', - 'CompanyAddress' => '公司地址', - 'CompanyRegion' => '北京', - 'CompanyCity' => '北京', - 'CompanyCountry' => 'CN', - 'CompanyDivision' => '部门', - 'CompanyPhone' => $this->config['phone'], - 'CompanyPostalCode' => '110100', - 'AdminName' => $this->config['username'], - 'AdminPhone' => $this->config['phone'], - 'AdminEmail' => $this->config['email'], - 'AdminTitle' => '职员', - 'DVAuthMethod' => 'DNS' - ]; - $data = $this->request('ComplementCSRInfo', $param); - - sleep(3); - - $param = [ - 'CertificateID' => $order['CertificateID'], - ]; - $data = $this->request('GetDVAuthInfo', $param); - - $dnsList = []; - if (!empty($data['Auths'])) { - foreach ($data['Auths'] as $auth) { - $mainDomain = getMainDomain($auth['Domain']); - $name = substr($auth['AuthKey'], 0, -(strlen($mainDomain) + 1)); - $dnsList[$mainDomain][] = ['name' => $name, 'type' => $auth['AuthType'] == 'DNS_TXT' ? 'TXT' : 'CNAME', 'value' => $auth['AuthValue']]; - } - } - return $dnsList; - } - - public function authOrder($domainList, $order) {} - - public function getAuthStatus($domainList, $order) - { - $param = [ - 'CertificateID' => $order['CertificateID'], - ]; - $data = $this->request('GetCertificateDetailInfo', $param); - if ($data['CertificateInfo']['StateCode'] == 'COMPLETED' || $data['CertificateInfo']['StateCode'] == 'RENEWED') { - return true; - } elseif ($data['CertificateInfo']['StateCode'] == 'REJECTED' || $data['CertificateInfo']['StateCode'] == 'SECURITY_REVIEW_FAILED') { - throw new Exception('证书审核失败:' . $data['CertificateInfo']['State']); - } else { - return false; - } - } - - public function finalizeOrder($domainList, $order, $keytype, $keysize) - { - if (!is_dir(app()->getRuntimePath() . 'cert')) mkdir(app()->getRuntimePath() . 'cert'); - $param = [ - 'CertificateID' => $order['CertificateID'], - ]; - $info = $this->request('GetCertificateDetailInfo', $param); - - $data = $this->request('DownloadCertificate', $param); - $file_data = get_curl($data['CertificateUrl']); - $file_path = app()->getRuntimePath() . 'cert/USSL_' . $order['CertificateID'] . '.zip'; - file_put_contents($file_path, $file_data); - - $zip = new \ZipArchive; - if ($zip->open($file_path) === true) { - $zip->extractTo(app()->getRuntimePath() . 'cert/'); - $zip->close(); - } else { - throw new Exception('解压证书失败'); - } - $cert_dir = app()->getRuntimePath() . 'cert/Nginx'; - - $items = scandir($cert_dir); - if ($items === false) throw new Exception('解压后的证书文件夹不存在'); - $private_key = null; - $fullchain = null; - foreach ($items as $item) { - if (substr($item, -4) == '.key') { - $private_key = file_get_contents($cert_dir . '/' . $item); - } elseif (substr($item, -4) == '.pem') { - $fullchain = file_get_contents($cert_dir . '/' . $item); - } - } - if (empty($private_key) || empty($fullchain)) throw new Exception('解压后的证书文件夹内未找到证书文件'); - - clearDirectory(app()->getRuntimePath() . 'cert'); - - return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $info['CertificateInfo']['CaOrganization'], 'subject' => $info['CertificateInfo']['Name'], 'validFrom' => $info['CertificateInfo']['IssuedDate'], 'validTo' => $info['CertificateInfo']['ExpiredDate']]; - } - - public function revoke($order, $pem) - { - $param = [ - 'CertificateID' => $order['CertificateID'], - 'Reason' => '业务终止', - ]; - $this->request('RevokeCertificate', $param); - } - - public function cancel($order) - { - $param = [ - 'CertificateID' => $order['CertificateID'], - ]; - $this->request('CancelCertificateOrder', $param); - - sleep(1); - - $param['CertificateMode'] = 'purchase'; - $this->request('DeleteSSLCertificate', $param); - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - private function request($action, $params) - { - $this->log('Action:' . $action . PHP_EOL . 'Request:' . json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); - $result = $this->client->request($action, $params); - $this->log('Response:' . json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); - return $result; - } -} +PublicKey = $config['PublicKey']; + $this->PrivateKey = $config['PrivateKey']; + $this->client = new UcloudClient($this->PublicKey, $this->PrivateKey); + $this->config = $config; + } + + public function register() + { + if (empty($this->PublicKey) || empty($this->PrivateKey) || empty($this->config['username']) || empty($this->config['phone']) || empty($this->config['email'])) throw new Exception('必填参数不能为空'); + $param = ['Mode' => 'free']; + $this->request('GetCertificateList', $param); + return true; + } + + public function buyCert($domainList, &$order) + { + $param = [ + 'CertificateBrand' => 'TrustAsia', + 'CertificateName' => 'TrustAsiaC1DVFree', + 'DomainsCount' => 1, + 'ValidYear' => 1, + ]; + $data = $this->request('PurchaseCertificate', $param); + if (!isset($data['CertificateID'])) throw new Exception('证书购买失败,CertificateID为空'); + $order['CertificateID'] = $data['CertificateID']; + } + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + $domain = $domainList[0]; + $param = [ + 'CertificateID' => $order['CertificateID'], + 'Domains' => $domain, + 'CSROnline' => 1, + 'CSREncryptAlgo' => ['RSA' => 'RSA', 'ECC' => 'ECDSA'][$keytype], + 'CSRKeyParameter' => ['2048' => '2048', '3072' => '3072', '256' => 'prime256v1', '384' => 'prime384v1'][$keysize], + 'CompanyName' => '公司名称', + 'CompanyAddress' => '公司地址', + 'CompanyRegion' => '北京', + 'CompanyCity' => '北京', + 'CompanyCountry' => 'CN', + 'CompanyDivision' => '部门', + 'CompanyPhone' => $this->config['phone'], + 'CompanyPostalCode' => '110100', + 'AdminName' => $this->config['username'], + 'AdminPhone' => $this->config['phone'], + 'AdminEmail' => $this->config['email'], + 'AdminTitle' => '职员', + 'DVAuthMethod' => 'DNS' + ]; + $data = $this->request('ComplementCSRInfo', $param); + + sleep(3); + + $param = [ + 'CertificateID' => $order['CertificateID'], + ]; + $data = $this->request('GetDVAuthInfo', $param); + + $dnsList = []; + if (!empty($data['Auths'])) { + foreach ($data['Auths'] as $auth) { + $mainDomain = getMainDomain($auth['Domain']); + $name = substr($auth['AuthKey'], 0, -(strlen($mainDomain) + 1)); + $dnsList[$mainDomain][] = ['name' => $name, 'type' => $auth['AuthType'] == 'DNS_TXT' ? 'TXT' : 'CNAME', 'value' => $auth['AuthValue']]; + } + } + return $dnsList; + } + + public function authOrder($domainList, $order) {} + + public function getAuthStatus($domainList, $order) + { + $param = [ + 'CertificateID' => $order['CertificateID'], + ]; + $data = $this->request('GetCertificateDetailInfo', $param); + if ($data['CertificateInfo']['StateCode'] == 'COMPLETED' || $data['CertificateInfo']['StateCode'] == 'RENEWED') { + return true; + } elseif ($data['CertificateInfo']['StateCode'] == 'REJECTED' || $data['CertificateInfo']['StateCode'] == 'SECURITY_REVIEW_FAILED') { + throw new Exception('证书审核失败:' . $data['CertificateInfo']['State']); + } else { + return false; + } + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + if (!is_dir(app()->getRuntimePath() . 'cert')) mkdir(app()->getRuntimePath() . 'cert'); + $param = [ + 'CertificateID' => $order['CertificateID'], + ]; + $info = $this->request('GetCertificateDetailInfo', $param); + + $data = $this->request('DownloadCertificate', $param); + $file_data = get_curl($data['CertificateUrl']); + $file_path = app()->getRuntimePath() . 'cert/USSL_' . $order['CertificateID'] . '.zip'; + file_put_contents($file_path, $file_data); + + $zip = new \ZipArchive; + if ($zip->open($file_path) === true) { + $zip->extractTo(app()->getRuntimePath() . 'cert/'); + $zip->close(); + } else { + throw new Exception('解压证书失败'); + } + $cert_dir = app()->getRuntimePath() . 'cert/Nginx'; + + $items = scandir($cert_dir); + if ($items === false) throw new Exception('解压后的证书文件夹不存在'); + $private_key = null; + $fullchain = null; + foreach ($items as $item) { + if (substr($item, -4) == '.key') { + $private_key = file_get_contents($cert_dir . '/' . $item); + } elseif (substr($item, -4) == '.pem') { + $fullchain = file_get_contents($cert_dir . '/' . $item); + } + } + if (empty($private_key) || empty($fullchain)) throw new Exception('解压后的证书文件夹内未找到证书文件'); + + clearDirectory(app()->getRuntimePath() . 'cert'); + + return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $info['CertificateInfo']['CaOrganization'], 'subject' => $info['CertificateInfo']['Name'], 'validFrom' => $info['CertificateInfo']['IssuedDate'], 'validTo' => $info['CertificateInfo']['ExpiredDate']]; + } + + public function revoke($order, $pem) + { + $param = [ + 'CertificateID' => $order['CertificateID'], + 'Reason' => '业务终止', + ]; + $this->request('RevokeCertificate', $param); + } + + public function cancel($order) + { + $param = [ + 'CertificateID' => $order['CertificateID'], + ]; + $this->request('CancelCertificateOrder', $param); + + sleep(1); + + $param['CertificateMode'] = 'purchase'; + $this->request('DeleteSSLCertificate', $param); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($action, $params) + { + $this->log('Action:' . $action . PHP_EOL . 'Request:' . json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + $result = $this->client->request($action, $params); + $this->log('Response:' . json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return $result; + } +} diff --git a/app/lib/cert/zerossl.php b/app/lib/cert/zerossl.php index ebdbbfc..bfcf4ad 100644 --- a/app/lib/cert/zerossl.php +++ b/app/lib/cert/zerossl.php @@ -1,134 +1,134 @@ -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 (isset($this->config['eabMode']) && $this->config['eabMode'] == 'auto') { - $eab = $this->getEAB($this->config['email']); - } else { - $eab = ['kid' => $this->config['kid'], 'key' => $this->config['key']]; - } - - if (!empty($this->ext['key'])) { - $kid = $this->ac->registerEAB(true, $eab['kid'], $eab['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, $eab['kid'], $eab['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'])) { - foreach ($order['challenges'] as $opts) { - $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 "sectigo.com"']; - }*/ - $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; - } - } - - 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); - } - - private function getEAB($email) - { - $api = "https://api.zerossl.com/acme/eab-credentials-email"; - $response = http_request($api, http_build_query(['email' => $email]), null, null, null, $this->config['proxy'] == 1); - $result = json_decode($response['body'], true); - if (!isset($result['success'])) { - throw new Exception('获取EAB失败:' . $response['body']); - } elseif (!$result['success'] && isset($result['error'])) { - throw new Exception('获取EAB失败:' . $result['error']['code'] . ' - ' . $result['error']['type']); - } elseif (!isset($result['eab_kid']) || !isset($result['eab_hmac_key'])) { - throw new Exception('获取EAB失败:返回数据不完整'); - } - return ['kid' => $result['eab_kid'], 'key' => $result['eab_hmac_key']]; - } -} +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 (isset($this->config['eabMode']) && $this->config['eabMode'] == 'auto') { + $eab = $this->getEAB($this->config['email']); + } else { + $eab = ['kid' => $this->config['kid'], 'key' => $this->config['key']]; + } + + if (!empty($this->ext['key'])) { + $kid = $this->ac->registerEAB(true, $eab['kid'], $eab['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, $eab['kid'], $eab['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'])) { + foreach ($order['challenges'] as $opts) { + $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 "sectigo.com"']; + }*/ + $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; + } + } + + 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); + } + + private function getEAB($email) + { + $api = "https://api.zerossl.com/acme/eab-credentials-email"; + $response = http_request($api, http_build_query(['email' => $email]), null, null, null, $this->config['proxy'] == 1); + $result = json_decode($response['body'], true); + if (!isset($result['success'])) { + throw new Exception('获取EAB失败:' . $response['body']); + } elseif (!$result['success'] && isset($result['error'])) { + throw new Exception('获取EAB失败:' . $result['error']['code'] . ' - ' . $result['error']['type']); + } elseif (!isset($result['eab_kid']) || !isset($result['eab_hmac_key'])) { + throw new Exception('获取EAB失败:返回数据不完整'); + } + return ['kid' => $result['eab_kid'], 'key' => $result['eab_hmac_key']]; + } +} diff --git a/app/lib/client/AWS.php b/app/lib/client/AWS.php index 170dd50..634e268 100644 --- a/app/lib/client/AWS.php +++ b/app/lib/client/AWS.php @@ -1,364 +1,364 @@ -AccessKeyId = $AccessKeyId; - $this->SecretAccessKey = $SecretAccessKey; - $this->endpoint = $endpoint; - $this->service = $service; - $this->version = $version; - $this->region = $region; - $this->proxy = $proxy; - } - - /** - * @param string $method 请求方法 - * @param string $action 方法名称 - * @param array $params 请求参数 - * @return array - * @throws Exception - */ - public function request($method, $action, $params = []) - { - if (!empty($params)) { - $params = array_filter($params, function ($a) { - return $a !== null; - }); - } - - $body = ''; - $query = []; - if ($method == 'GET' || $method == 'DELETE') { - $query = $params; - } else { - $body = !empty($params) ? json_encode($params) : ''; - } - - $time = time(); - $date = gmdate("Ymd\THis\Z", $time); - $headers = [ - 'Host' => $this->endpoint, - 'X-Amz-Target' => $action, - 'X-Amz-Date' => $date, - //'X-Amz-Content-Sha256' => hash("sha256", $body), - ]; - if ($body) { - $headers['Content-Type'] = 'application/x-amz-json-1.1'; - } - $path = '/'; - - $authorization = $this->generateSign($method, $path, $query, $headers, $body, $date); - $headers['Authorization'] = $authorization; - - $url = 'https://' . $this->endpoint . $path; - if (!empty($query)) { - $url .= '?' . http_build_query($query); - } - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key . ': ' . $value; - } - return $this->curl($method, $url, $body, $header); - } - - /** - * @param string $method 请求方法 - * @param string $action 方法名称 - * @param array $params 请求参数 - * @return array - * @throws Exception - */ - public function requestXml($method, $action, $params = []) - { - if (!empty($params)) { - $params = array_filter($params, function ($a) { - return $a !== null; - }); - } - - $body = ''; - $query = [ - 'Action' => $action, - 'Version' => $this->version, - ]; - if ($method == 'GET' || $method == 'DELETE') { - $query = array_merge($query, $params); - } else { - $body = !empty($params) ? http_build_query($params) : ''; - } - - $time = time(); - $date = gmdate("Ymd\THis\Z", $time); - $headers = [ - 'Host' => $this->endpoint, - 'X-Amz-Date' => $date, - ]; - - $path = '/'; - $authorization = $this->generateSign($method, $path, $query, $headers, $body, $date); - $headers['Authorization'] = $authorization; - - $url = 'https://' . $this->endpoint . $path; - if (!empty($query)) { - $url .= '?' . http_build_query($query); - } - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key . ': ' . $value; - } - return $this->curl($method, $url, $body, $header, true); - } - - /** - * @param string $method 请求方法 - * @param string $path 请求路径 - * @param array $params 请求参数 - * @param \SimpleXMLElement $xml 请求XML - * @return array - * @throws Exception - */ - public function requestXmlN($method, $path, $params = [], $xml = null, $etag = false) - { - if (!empty($params)) { - $params = array_filter($params, function ($a) { - return $a !== null; - }); - } - - $path = '/' . $this->version . $path; - $body = ''; - $query = []; - if ($method == 'GET' || $method == 'DELETE') { - $query = $params; - } else { - $body = !empty($params) ? $this->array2xml($params, $xml) : ''; - } - - $time = time(); - $date = gmdate("Ymd\THis\Z", $time); - $headers = [ - 'Host' => $this->endpoint, - 'X-Amz-Date' => $date, - //'X-Amz-Content-Sha256' => hash("sha256", $body), - ]; - if ($this->etag) { - $headers['If-Match'] = $this->etag; - } - - $authorization = $this->generateSign($method, $path, $query, $headers, $body, $date); - $headers['Authorization'] = $authorization; - - $url = 'https://' . $this->endpoint . $path; - if (!empty($query)) { - $url .= '?' . http_build_query($query); - } - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key . ': ' . $value; - } - return $this->curl($method, $url, $body, $header, true, $etag); - } - - private function generateSign($method, $path, $query, $headers, $body, $date) - { - $algorithm = "AWS4-HMAC-SHA256"; - - // step 1: build canonical request string - $httpRequestMethod = $method; - $canonicalUri = $this->getCanonicalURI($path); - $canonicalQueryString = $this->getCanonicalQueryString($query); - [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); - $hashedRequestPayload = hash("sha256", $body); - $canonicalRequest = $httpRequestMethod . "\n" - . $canonicalUri . "\n" - . $canonicalQueryString . "\n" - . $canonicalHeaders . "\n" - . $signedHeaders . "\n" - . $hashedRequestPayload; - - // step 2: build string to sign - $shortDate = substr($date, 0, 8); - $credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/aws4_request'; - $hashedCanonicalRequest = hash("sha256", $canonicalRequest); - $stringToSign = $algorithm . "\n" - . $date . "\n" - . $credentialScope . "\n" - . $hashedCanonicalRequest; - - // step 3: sign string - $kDate = hash_hmac("sha256", $shortDate, 'AWS4' . $this->SecretAccessKey, true); - $kRegion = hash_hmac("sha256", $this->region, $kDate, true); - $kService = hash_hmac("sha256", $this->service, $kRegion, true); - $kSigning = hash_hmac("sha256", "aws4_request", $kService, true); - $signature = hash_hmac("sha256", $stringToSign, $kSigning); - - // step 4: build authorization - $credential = $this->AccessKeyId . '/' . $credentialScope; - $authorization = $algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; - - return $authorization; - } - - private function escape($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } - - private function getCanonicalURI($path) - { - if (empty($path)) return '/'; - $pattens = explode('/', $path); - $pattens = array_map(function ($item) { - return $this->escape($item); - }, $pattens); - $canonicalURI = implode('/', $pattens); - return $canonicalURI; - } - - private function getCanonicalQueryString($parameters) - { - if (empty($parameters)) return ''; - ksort($parameters); - $canonicalQueryString = ''; - foreach ($parameters as $key => $value) { - $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); - } - return substr($canonicalQueryString, 1); - } - - private function getCanonicalHeaders($oldheaders) - { - $headers = array(); - foreach ($oldheaders as $key => $value) { - $headers[strtolower($key)] = trim($value); - } - ksort($headers); - - $canonicalHeaders = ''; - $signedHeaders = ''; - foreach ($headers as $key => $value) { - $canonicalHeaders .= $key . ':' . $value . "\n"; - $signedHeaders .= $key . ';'; - } - $signedHeaders = substr($signedHeaders, 0, -1); - return [$canonicalHeaders, $signedHeaders]; - } - - private function curl($method, $url, $body, $header, $xml = false, $etag = false) - { - $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); - } - if ($etag) { - curl_setopt($ch, CURLOPT_HEADER, true); - } - $response = curl_exec($ch); - $errno = curl_errno($ch); - if ($errno) { - $errmsg = curl_error($ch); - curl_close($ch); - throw new Exception('Curl error: ' . $errmsg); - } - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($etag) { - if (preg_match('/ETag: ([^\r\n]+)/', $response, $matches)) { - $this->etag = trim($matches[1]); - } - $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); - $response = substr($response, $headerSize); - } - curl_close($ch); - - if ($httpCode >= 200 && $httpCode < 300) { - if (empty($response)) return true; - return $xml ? $this->xml2array($response) : json_decode($response, true); - } - if ($xml) { - $arr = $this->xml2array($response); - if (isset($arr['Error']['Message'])) { - throw new Exception($arr['Error']['Message']); - } else { - throw new Exception('HTTP Code: ' . $httpCode); - } - } else { - $arr = json_decode($response, true); - if (isset($arr['message'])) { - throw new Exception($arr['message']); - } else { - throw new Exception('HTTP Code: ' . $httpCode); - } - } - } - - 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 array2xml($array, $xml = null, $parentTagName = 'root') - { - if ($xml === null) { - $xml = new \SimpleXMLElement(''); - } - - foreach ($array as $key => $value) { - // 确定当前标签名:如果是数字键名,使用父级标签名,否则使用当前键名 - $tagName = is_numeric($key) ? $parentTagName : $key; - - if (is_array($value)) { - // 检查数组的第一个子节点的键是否为0 - $firstKey = array_key_first($value); - $isFirstKeyZero = ($firstKey === 0 || $firstKey === '0'); - - if ($isFirstKeyZero) { - // 如果第一个子节点的键是0,则不生成当前节点标签,直接递归子节点 - $this->array2xml($value, $xml, $tagName); - - } else { - // 否则生成当前节点标签,并递归子节点 - $subNode = $xml->addChild($tagName); - $this->array2xml($value, $subNode, $tagName); - } - - } else { - $xml->addChild($key, $value); - } - } - - return $xml->asXML(); - } -} +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->endpoint = $endpoint; + $this->service = $service; + $this->version = $version; + $this->region = $region; + $this->proxy = $proxy; + } + + /** + * @param string $method 请求方法 + * @param string $action 方法名称 + * @param array $params 请求参数 + * @return array + * @throws Exception + */ + public function request($method, $action, $params = []) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + $body = ''; + $query = []; + if ($method == 'GET' || $method == 'DELETE') { + $query = $params; + } else { + $body = !empty($params) ? json_encode($params) : ''; + } + + $time = time(); + $date = gmdate("Ymd\THis\Z", $time); + $headers = [ + 'Host' => $this->endpoint, + 'X-Amz-Target' => $action, + 'X-Amz-Date' => $date, + //'X-Amz-Content-Sha256' => hash("sha256", $body), + ]; + if ($body) { + $headers['Content-Type'] = 'application/x-amz-json-1.1'; + } + $path = '/'; + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $date); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + /** + * @param string $method 请求方法 + * @param string $action 方法名称 + * @param array $params 请求参数 + * @return array + * @throws Exception + */ + public function requestXml($method, $action, $params = []) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + $body = ''; + $query = [ + 'Action' => $action, + 'Version' => $this->version, + ]; + if ($method == 'GET' || $method == 'DELETE') { + $query = array_merge($query, $params); + } else { + $body = !empty($params) ? http_build_query($params) : ''; + } + + $time = time(); + $date = gmdate("Ymd\THis\Z", $time); + $headers = [ + 'Host' => $this->endpoint, + 'X-Amz-Date' => $date, + ]; + + $path = '/'; + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $date); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header, true); + } + + /** + * @param string $method 请求方法 + * @param string $path 请求路径 + * @param array $params 请求参数 + * @param \SimpleXMLElement $xml 请求XML + * @return array + * @throws Exception + */ + public function requestXmlN($method, $path, $params = [], $xml = null, $etag = false) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + $path = '/' . $this->version . $path; + $body = ''; + $query = []; + if ($method == 'GET' || $method == 'DELETE') { + $query = $params; + } else { + $body = !empty($params) ? $this->array2xml($params, $xml) : ''; + } + + $time = time(); + $date = gmdate("Ymd\THis\Z", $time); + $headers = [ + 'Host' => $this->endpoint, + 'X-Amz-Date' => $date, + //'X-Amz-Content-Sha256' => hash("sha256", $body), + ]; + if ($this->etag) { + $headers['If-Match'] = $this->etag; + } + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $date); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header, true, $etag); + } + + private function generateSign($method, $path, $query, $headers, $body, $date) + { + $algorithm = "AWS4-HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $this->getCanonicalURI($path); + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + $canonicalRequest = $httpRequestMethod . "\n" + . $canonicalUri . "\n" + . $canonicalQueryString . "\n" + . $canonicalHeaders . "\n" + . $signedHeaders . "\n" + . $hashedRequestPayload; + + // step 2: build string to sign + $shortDate = substr($date, 0, 8); + $credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/aws4_request'; + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = $algorithm . "\n" + . $date . "\n" + . $credentialScope . "\n" + . $hashedCanonicalRequest; + + // step 3: sign string + $kDate = hash_hmac("sha256", $shortDate, 'AWS4' . $this->SecretAccessKey, true); + $kRegion = hash_hmac("sha256", $this->region, $kDate, true); + $kService = hash_hmac("sha256", $this->service, $kRegion, true); + $kSigning = hash_hmac("sha256", "aws4_request", $kService, true); + $signature = hash_hmac("sha256", $stringToSign, $kSigning); + + // step 4: build authorization + $credential = $this->AccessKeyId . '/' . $credentialScope; + $authorization = $algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalURI($path) + { + if (empty($path)) return '/'; + $pattens = explode('/', $path); + $pattens = array_map(function ($item) { + return $this->escape($item); + }, $pattens); + $canonicalURI = implode('/', $pattens); + return $canonicalURI; + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header, $xml = false, $etag = false) + { + $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); + } + if ($etag) { + curl_setopt($ch, CURLOPT_HEADER, true); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + $errmsg = curl_error($ch); + curl_close($ch); + throw new Exception('Curl error: ' . $errmsg); + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($etag) { + if (preg_match('/ETag: ([^\r\n]+)/', $response, $matches)) { + $this->etag = trim($matches[1]); + } + $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $response = substr($response, $headerSize); + } + curl_close($ch); + + if ($httpCode >= 200 && $httpCode < 300) { + if (empty($response)) return true; + return $xml ? $this->xml2array($response) : json_decode($response, true); + } + if ($xml) { + $arr = $this->xml2array($response); + if (isset($arr['Error']['Message'])) { + throw new Exception($arr['Error']['Message']); + } else { + throw new Exception('HTTP Code: ' . $httpCode); + } + } else { + $arr = json_decode($response, true); + if (isset($arr['message'])) { + throw new Exception($arr['message']); + } else { + throw new Exception('HTTP Code: ' . $httpCode); + } + } + } + + 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 array2xml($array, $xml = null, $parentTagName = 'root') + { + if ($xml === null) { + $xml = new \SimpleXMLElement(''); + } + + foreach ($array as $key => $value) { + // 确定当前标签名:如果是数字键名,使用父级标签名,否则使用当前键名 + $tagName = is_numeric($key) ? $parentTagName : $key; + + if (is_array($value)) { + // 检查数组的第一个子节点的键是否为0 + $firstKey = array_key_first($value); + $isFirstKeyZero = ($firstKey === 0 || $firstKey === '0'); + + if ($isFirstKeyZero) { + // 如果第一个子节点的键是0,则不生成当前节点标签,直接递归子节点 + $this->array2xml($value, $xml, $tagName); + + } else { + // 否则生成当前节点标签,并递归子节点 + $subNode = $xml->addChild($tagName); + $this->array2xml($value, $subNode, $tagName); + } + + } else { + $xml->addChild($key, $value); + } + } + + return $xml->asXML(); + } +} diff --git a/app/lib/client/Aliyun.php b/app/lib/client/Aliyun.php index 42575bb..9ad9efe 100644 --- a/app/lib/client/Aliyun.php +++ b/app/lib/client/Aliyun.php @@ -1,101 +1,101 @@ -AccessKeyId = $AccessKeyId; - $this->AccessKeySecret = $AccessKeySecret; - $this->Endpoint = $Endpoint; - $this->Version = $Version; - $this->proxy = $proxy; - } - - /** - * @param array $param 请求参数 - * @return bool|array - * @throws Exception - */ - public function request($param, $method = 'POST') - { - $url = 'https://' . $this->Endpoint . '/'; - $data = array( - 'Format' => 'JSON', - 'Version' => $this->Version, - 'AccessKeyId' => $this->AccessKeyId, - 'SignatureMethod' => 'HMAC-SHA1', - 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), - 'SignatureVersion' => '1.0', - 'SignatureNonce' => random(8) - ); - $data = array_merge($data, $param); - $data['Signature'] = $this->aliyunSignature($data, $this->AccessKeySecret, $method); - if ($method == 'GET') { - $url .= '?' . http_build_query($data); - } - $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_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - if ($method == 'POST') { - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); - } - $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); - - $arr = json_decode($response, true); - if ($httpCode == 200) { - return $arr; - } elseif ($arr) { - throw new Exception($arr['Message']); - } else { - throw new Exception('返回数据解析失败'); - } - } - - private function aliyunSignature($parameters, $accessKeySecret, $method) - { - ksort($parameters); - $canonicalizedQueryString = ''; - foreach ($parameters as $key => $value) { - if ($value === null) continue; - $canonicalizedQueryString .= '&' . $this->percentEncode($key) . '=' . $this->percentEncode($value); - } - $stringToSign = $method . '&%2F&' . $this->percentEncode(substr($canonicalizedQueryString, 1)); - $signature = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&", true)); - - return $signature; - } - - private function percentEncode($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } -} +AccessKeyId = $AccessKeyId; + $this->AccessKeySecret = $AccessKeySecret; + $this->Endpoint = $Endpoint; + $this->Version = $Version; + $this->proxy = $proxy; + } + + /** + * @param array $param 请求参数 + * @return bool|array + * @throws Exception + */ + public function request($param, $method = 'POST') + { + $url = 'https://' . $this->Endpoint . '/'; + $data = array( + 'Format' => 'JSON', + 'Version' => $this->Version, + 'AccessKeyId' => $this->AccessKeyId, + 'SignatureMethod' => 'HMAC-SHA1', + 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), + 'SignatureVersion' => '1.0', + 'SignatureNonce' => random(8) + ); + $data = array_merge($data, $param); + $data['Signature'] = $this->aliyunSignature($data, $this->AccessKeySecret, $method); + if ($method == 'GET') { + $url .= '?' . http_build_query($data); + } + $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_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + if ($method == 'POST') { + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); + } + $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); + + $arr = json_decode($response, true); + if ($httpCode == 200) { + return $arr; + } elseif ($arr) { + throw new Exception($arr['Message']); + } else { + throw new Exception('返回数据解析失败'); + } + } + + private function aliyunSignature($parameters, $accessKeySecret, $method) + { + ksort($parameters); + $canonicalizedQueryString = ''; + foreach ($parameters as $key => $value) { + if ($value === null) continue; + $canonicalizedQueryString .= '&' . $this->percentEncode($key) . '=' . $this->percentEncode($value); + } + $stringToSign = $method . '&%2F&' . $this->percentEncode(substr($canonicalizedQueryString, 1)); + $signature = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&", true)); + + return $signature; + } + + private function percentEncode($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } +} diff --git a/app/lib/client/AliyunNew.php b/app/lib/client/AliyunNew.php index b59d25e..f3dd8ce 100644 --- a/app/lib/client/AliyunNew.php +++ b/app/lib/client/AliyunNew.php @@ -1,188 +1,188 @@ -AccessKeyId = $AccessKeyId; - $this->AccessKeySecret = $AccessKeySecret; - $this->Endpoint = $Endpoint; - $this->Version = $Version; - $this->proxy = $proxy; - } - - /** - * @param string $method 请求方法 - * @param string $action 操作名称 - * @param array|null $params 请求参数 - * @return array - * @throws Exception - */ - public function request($method, $action, $path = '/', $params = null) - { - if (!empty($params)) { - $params = array_filter($params, function ($a) { - return $a !== null; - }); - } - - if ($method == 'GET' || $method == 'DELETE') { - $query = $params; - $body = ''; - } else { - $query = []; - $body = !empty($params) ? json_encode($params) : ''; - } - $headers = [ - 'x-acs-action' => $action, - 'x-acs-version' => $this->Version, - 'x-acs-signature-nonce' => md5(uniqid(mt_rand(), true) . microtime()), - 'x-acs-date' => gmdate('Y-m-d\TH:i:s\Z'), - 'x-acs-content-sha256' => hash("sha256", $body), - 'Host' => $this->Endpoint, - ]; - if ($body) { - $headers['Content-Type'] = 'application/json; charset=utf-8'; - } - - $authorization = $this->generateSign($method, $path, $query, $headers, $body); - $headers['Authorization'] = $authorization; - - $url = 'https://' . $this->Endpoint . $path; - if (!empty($query)) { - $url .= '?' . http_build_query($query); - } - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key . ': ' . $value; - } - return $this->curl($method, $url, $body, $header); - } - - private function generateSign($method, $path, $query, $headers, $body) - { - $algorithm = "ACS3-HMAC-SHA256"; - - // step 1: build canonical request string - $httpRequestMethod = $method; - $canonicalUri = $this->getCanonicalURI($path); - $canonicalQueryString = $this->getCanonicalQueryString($query); - [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); - $hashedRequestPayload = hash("sha256", $body); - $canonicalRequest = $httpRequestMethod . "\n" - . $canonicalUri . "\n" - . $canonicalQueryString . "\n" - . $canonicalHeaders . "\n" - . $signedHeaders . "\n" - . $hashedRequestPayload; - - // step 2: build string to sign - $hashedCanonicalRequest = hash("sha256", $canonicalRequest); - $stringToSign = $algorithm . "\n" - . $hashedCanonicalRequest; - - // step 3: sign string - $signature = hash_hmac("sha256", $stringToSign, $this->AccessKeySecret); - - // step 4: build authorization - $authorization = $algorithm . ' Credential=' . $this->AccessKeyId . ',SignedHeaders=' . $signedHeaders . ',Signature=' . $signature; - - return $authorization; - } - - private function escape($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } - - private function getCanonicalURI($path) - { - if (empty($path)) return '/'; - $pattens = explode('/', $path); - $pattens = array_map(function ($item) { - return $this->escape($item); - }, $pattens); - $canonicalURI = implode('/', $pattens); - return $canonicalURI; - } - - private function getCanonicalQueryString($parameters) - { - if (empty($parameters)) return ''; - ksort($parameters); - $canonicalQueryString = ''; - foreach ($parameters as $key => $value) { - $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); - } - return substr($canonicalQueryString, 1); - } - - private function getCanonicalHeaders($oldheaders) - { - $headers = array(); - foreach ($oldheaders as $key => $value) { - $headers[strtolower($key)] = trim($value); - } - ksort($headers); - - $canonicalHeaders = ''; - $signedHeaders = ''; - foreach ($headers as $key => $value) { - $canonicalHeaders .= $key . ':' . $value . "\n"; - $signedHeaders .= $key . ';'; - } - $signedHeaders = substr($signedHeaders, 0, -1); - return [$canonicalHeaders, $signedHeaders]; - } - - private function curl($method, $url, $body, $header) - { - $ch = curl_init($url); - if ($this->proxy) { - curl_set_proxy($ch); - } - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - if (!empty($body)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - $response = curl_exec($ch); - $errno = curl_errno($ch); - if ($errno) { - $errmsg = curl_error($ch); - curl_close($ch); - throw new Exception('Curl error: ' . $errmsg); - } - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - $arr = json_decode($response, true); - if ($httpCode == 200) { - return $arr; - } elseif ($arr) { - if (strpos($arr['Message'], '.') > 0) $arr['Message'] = substr($arr['Message'], 0, strpos($arr['Message'], '.') + 1); - throw new Exception($arr['Message']); - } else { - throw new Exception('返回数据解析失败'); - } - } -} +AccessKeyId = $AccessKeyId; + $this->AccessKeySecret = $AccessKeySecret; + $this->Endpoint = $Endpoint; + $this->Version = $Version; + $this->proxy = $proxy; + } + + /** + * @param string $method 请求方法 + * @param string $action 操作名称 + * @param array|null $params 请求参数 + * @return array + * @throws Exception + */ + public function request($method, $action, $path = '/', $params = null) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + if ($method == 'GET' || $method == 'DELETE') { + $query = $params; + $body = ''; + } else { + $query = []; + $body = !empty($params) ? json_encode($params) : ''; + } + $headers = [ + 'x-acs-action' => $action, + 'x-acs-version' => $this->Version, + 'x-acs-signature-nonce' => md5(uniqid(mt_rand(), true) . microtime()), + 'x-acs-date' => gmdate('Y-m-d\TH:i:s\Z'), + 'x-acs-content-sha256' => hash("sha256", $body), + 'Host' => $this->Endpoint, + ]; + if ($body) { + $headers['Content-Type'] = 'application/json; charset=utf-8'; + } + + $authorization = $this->generateSign($method, $path, $query, $headers, $body); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->Endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($method, $path, $query, $headers, $body) + { + $algorithm = "ACS3-HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $this->getCanonicalURI($path); + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + $canonicalRequest = $httpRequestMethod . "\n" + . $canonicalUri . "\n" + . $canonicalQueryString . "\n" + . $canonicalHeaders . "\n" + . $signedHeaders . "\n" + . $hashedRequestPayload; + + // step 2: build string to sign + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = $algorithm . "\n" + . $hashedCanonicalRequest; + + // step 3: sign string + $signature = hash_hmac("sha256", $stringToSign, $this->AccessKeySecret); + + // step 4: build authorization + $authorization = $algorithm . ' Credential=' . $this->AccessKeyId . ',SignedHeaders=' . $signedHeaders . ',Signature=' . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalURI($path) + { + if (empty($path)) return '/'; + $pattens = explode('/', $path); + $pattens = array_map(function ($item) { + return $this->escape($item); + }, $pattens); + $canonicalURI = implode('/', $pattens); + return $canonicalURI; + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + if ($this->proxy) { + curl_set_proxy($ch); + } + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + $errmsg = curl_error($ch); + curl_close($ch); + throw new Exception('Curl error: ' . $errmsg); + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $arr = json_decode($response, true); + if ($httpCode == 200) { + return $arr; + } elseif ($arr) { + if (strpos($arr['Message'], '.') > 0) $arr['Message'] = substr($arr['Message'], 0, strpos($arr['Message'], '.') + 1); + throw new Exception($arr['Message']); + } else { + throw new Exception('返回数据解析失败'); + } + } +} diff --git a/app/lib/client/AliyunOSS.php b/app/lib/client/AliyunOSS.php index 7d6108d..5ad4e70 100644 --- a/app/lib/client/AliyunOSS.php +++ b/app/lib/client/AliyunOSS.php @@ -1,267 +1,267 @@ -AccessKeyId = $AccessKeyId; - $this->AccessKeySecret = $AccessKeySecret; - $this->Endpoint = $Endpoint; - $this->proxy = $proxy; - } - - public function addBucketCnameCert($bucket, $domain, $cert_id) - { - $strXml = << - - - EOF; - $xml = new \SimpleXMLElement($strXml); - $node = $xml->addChild('Cname'); - $node->addChild('Domain', $domain); - $certNode = $node->addChild('CertificateConfiguration'); - $certNode->addChild('CertId', $cert_id); - $certNode->addChild('Force', 'true'); - $body = $xml->asXML(); - - $options = [ - 'bucket' => $bucket, - 'key' => '', - ]; - $query = [ - 'cname' => '', - 'comp' => 'add' - ]; - return $this->request('POST', '/', $query, $body, $options); - } - - public function deleteBucketCnameCert($bucket, $domain) - { - $strXml = << - - - EOF; - $xml = new \SimpleXMLElement($strXml); - $node = $xml->addChild('Cname'); - $node->addChild('Domain', $domain); - $certNode = $node->addChild('CertificateConfiguration'); - $certNode->addChild('DeleteCertificate', 'true'); - $body = $xml->asXML(); - - $options = [ - 'bucket' => $bucket, - 'key' => '', - ]; - $query = [ - 'cname' => '', - 'comp' => 'add' - ]; - return $this->request('POST', '/', $query, $body, $options); - } - - public function getBucketCname($bucket) - { - $options = [ - 'bucket' => $bucket, - 'key' => '', - ]; - $query = [ - 'cname' => '', - ]; - return $this->request('GET', '/', $query, null, $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', - '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->AccessKeySecret, true)); - return 'OSS ' . $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-oss-", 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", "uploads", "location", "cors", - "logging", "website", "referer", "lifecycle", - "delete", "append", "tagging", "objectMeta", - "uploadId", "partNumber", "security-token", "x-oss-security-token", - "position", "img", "style", "styleName", - "replication", "replicationProgress", - "replicationLocation", "cname", "bucketInfo", - "comp", "qos", "live", "status", "vod", - "startTime", "endTime", "symlink", - "x-oss-process", "response-content-type", "x-oss-traffic-limit", - "response-content-language", "response-expires", - "response-cache-control", "response-content-disposition", - "response-content-encoding", "udf", "udfName", "udfImage", - "udfId", "udfImageDesc", "udfApplication", - "udfApplicationLog", "restore", "callback", "callback-var", "qosInfo", - "policy", "stat", "encryption", "versions", "versioning", "versionId", "requestPayment", - "x-oss-request-payer", "sequential", - "inventory", "inventoryId", "continuation-token", "asyncFetch", - "worm", "wormId", "wormExtend", "withHashContext", - "x-oss-enable-md5", "x-oss-enable-sha1", "x-oss-enable-sha256", - "x-oss-hash-ctx", "x-oss-md5-ctx", "transferAcceleration", - "regionList", "cloudboxes", "x-oss-ac-source-ip", "x-oss-ac-subnet-mask", "x-oss-ac-vpc-id", "x-oss-ac-forward-allow", - "metaQuery", "resourceGroup", "rtc", "x-oss-async-process", "responseHeader" - ); +AccessKeyId = $AccessKeyId; + $this->AccessKeySecret = $AccessKeySecret; + $this->Endpoint = $Endpoint; + $this->proxy = $proxy; + } + + public function addBucketCnameCert($bucket, $domain, $cert_id) + { + $strXml = << + + + EOF; + $xml = new \SimpleXMLElement($strXml); + $node = $xml->addChild('Cname'); + $node->addChild('Domain', $domain); + $certNode = $node->addChild('CertificateConfiguration'); + $certNode->addChild('CertId', $cert_id); + $certNode->addChild('Force', 'true'); + $body = $xml->asXML(); + + $options = [ + 'bucket' => $bucket, + 'key' => '', + ]; + $query = [ + 'cname' => '', + 'comp' => 'add' + ]; + return $this->request('POST', '/', $query, $body, $options); + } + + public function deleteBucketCnameCert($bucket, $domain) + { + $strXml = << + + + EOF; + $xml = new \SimpleXMLElement($strXml); + $node = $xml->addChild('Cname'); + $node->addChild('Domain', $domain); + $certNode = $node->addChild('CertificateConfiguration'); + $certNode->addChild('DeleteCertificate', 'true'); + $body = $xml->asXML(); + + $options = [ + 'bucket' => $bucket, + 'key' => '', + ]; + $query = [ + 'cname' => '', + 'comp' => 'add' + ]; + return $this->request('POST', '/', $query, $body, $options); + } + + public function getBucketCname($bucket) + { + $options = [ + 'bucket' => $bucket, + 'key' => '', + ]; + $query = [ + 'cname' => '', + ]; + return $this->request('GET', '/', $query, null, $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', + '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->AccessKeySecret, true)); + return 'OSS ' . $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-oss-", 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", "uploads", "location", "cors", + "logging", "website", "referer", "lifecycle", + "delete", "append", "tagging", "objectMeta", + "uploadId", "partNumber", "security-token", "x-oss-security-token", + "position", "img", "style", "styleName", + "replication", "replicationProgress", + "replicationLocation", "cname", "bucketInfo", + "comp", "qos", "live", "status", "vod", + "startTime", "endTime", "symlink", + "x-oss-process", "response-content-type", "x-oss-traffic-limit", + "response-content-language", "response-expires", + "response-cache-control", "response-content-disposition", + "response-content-encoding", "udf", "udfName", "udfImage", + "udfId", "udfImageDesc", "udfApplication", + "udfApplicationLog", "restore", "callback", "callback-var", "qosInfo", + "policy", "stat", "encryption", "versions", "versioning", "versionId", "requestPayment", + "x-oss-request-payer", "sequential", + "inventory", "inventoryId", "continuation-token", "asyncFetch", + "worm", "wormId", "wormExtend", "withHashContext", + "x-oss-enable-md5", "x-oss-enable-sha1", "x-oss-enable-sha256", + "x-oss-hash-ctx", "x-oss-md5-ctx", "transferAcceleration", + "regionList", "cloudboxes", "x-oss-ac-source-ip", "x-oss-ac-subnet-mask", "x-oss-ac-vpc-id", "x-oss-ac-forward-allow", + "metaQuery", "resourceGroup", "rtc", "x-oss-async-process", "responseHeader" + ); } \ No newline at end of file diff --git a/app/lib/client/BaiduCloud.php b/app/lib/client/BaiduCloud.php index ba352a9..6da15c2 100644 --- a/app/lib/client/BaiduCloud.php +++ b/app/lib/client/BaiduCloud.php @@ -1,181 +1,181 @@ -AccessKeyId = $AccessKeyId; - $this->SecretAccessKey = $SecretAccessKey; - $this->endpoint = $endpoint; - $this->proxy = $proxy; - } - - /** - * @param string $method 请求方法 - * @param string $path 请求路径 - * @param array|null $query 请求参数 - * @param array|null $params 请求体 - * @return array - * @throws Exception - */ - public function request($method, $path, $query = null, $params = null) - { - if (!empty($query)) { - $query = array_filter($query, function ($a) { return $a !== null;}); - } - if (!empty($params)) { - $params = array_filter($params, function ($a) { return $a !== null;}); - } - - $time = time(); - $date = gmdate("Y-m-d\TH:i:s\Z", $time); - $body = !empty($params) ? json_encode($params) : ''; - $headers = [ - 'Host' => $this->endpoint, - 'x-bce-date' => $date, - ]; - if ($body) { - $headers['Content-Type'] = 'application/json'; - } - - $authorization = $this->generateSign($method, $path, $query, $headers, $time); - $headers['Authorization'] = $authorization; - - $url = 'https://'.$this->endpoint.$path; - if (!empty($query)) { - $url .= '?'.http_build_query($query); - } - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key.': '.$value; - } - return $this->curl($method, $url, $body, $header); - } - - private function generateSign($method, $path, $query, $headers, $time) - { - $algorithm = "bce-auth-v1"; - - // step 1: build canonical request string - $httpRequestMethod = $method; - $canonicalUri = $this->getCanonicalUri($path); - $canonicalQueryString = $this->getCanonicalQueryString($query); - [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); - $canonicalRequest = $httpRequestMethod."\n" - .$canonicalUri."\n" - .$canonicalQueryString."\n" - .$canonicalHeaders; - - // step 2: calculate signing key - $date = gmdate("Y-m-d\TH:i:s\Z", $time); - $expirationInSeconds = 1800; - $authString = $algorithm . '/' . $this->AccessKeyId . '/' . $date . '/' . $expirationInSeconds; - $signingKey = hash_hmac('sha256', $authString, $this->SecretAccessKey); - - // step 3: sign string - $signature = hash_hmac("sha256", $canonicalRequest, $signingKey); - - // step 4: build authorization - $authorization = $authString . '/' . $signedHeaders . "/" . $signature; - - return $authorization; - } - - private function escape($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } - - private function getCanonicalUri($path) - { - if (empty($path)) return '/'; - $uri = str_replace('%2F', '/', $this->escape($path)); - if (substr($uri, 0, 1) !== '/') $uri = '/' . $uri; - return $uri; - } - - private function getCanonicalQueryString($parameters) - { - if (empty($parameters)) return ''; - ksort($parameters); - $canonicalQueryString = ''; - foreach ($parameters as $key => $value) { - if ($key == 'authorization') continue; - $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); - } - return substr($canonicalQueryString, 1); - } - - private function getCanonicalHeaders($oldheaders) - { - $headers = array(); - foreach ($oldheaders as $key => $value) { - $headers[strtolower($key)] = trim($value); - } - ksort($headers); - - $canonicalHeaders = ''; - $signedHeaders = ''; - foreach ($headers as $key => $value) { - $canonicalHeaders .= $this->escape($key) . ':' . $this->escape($value) . "\n"; - $signedHeaders .= $key . ';'; - } - $canonicalHeaders = substr($canonicalHeaders, 0, -1); - $signedHeaders = substr($signedHeaders, 0, -1); - return [$canonicalHeaders, $signedHeaders]; - } - - private function curl($method, $url, $body, $header) - { - $ch = curl_init($url); - if ($this->proxy) { - curl_set_proxy($ch); - } - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - if (!empty($body)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - $response = curl_exec($ch); - $errno = curl_errno($ch); - $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 (empty($response) && $httpCode == 200) { - return true; - } - $arr = json_decode($response, true); - if ($arr) { - if (isset($arr['code']) && isset($arr['message'])) { - throw new Exception($arr['message']); - } else { - return $arr; - } - } else { - throw new Exception('返回数据解析失败'); - } - } +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->endpoint = $endpoint; + $this->proxy = $proxy; + } + + /** + * @param string $method 请求方法 + * @param string $path 请求路径 + * @param array|null $query 请求参数 + * @param array|null $params 请求体 + * @return array + * @throws Exception + */ + public function request($method, $path, $query = null, $params = null) + { + if (!empty($query)) { + $query = array_filter($query, function ($a) { return $a !== null;}); + } + if (!empty($params)) { + $params = array_filter($params, function ($a) { return $a !== null;}); + } + + $time = time(); + $date = gmdate("Y-m-d\TH:i:s\Z", $time); + $body = !empty($params) ? json_encode($params) : ''; + $headers = [ + 'Host' => $this->endpoint, + 'x-bce-date' => $date, + ]; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + + $authorization = $this->generateSign($method, $path, $query, $headers, $time); + $headers['Authorization'] = $authorization; + + $url = 'https://'.$this->endpoint.$path; + if (!empty($query)) { + $url .= '?'.http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key.': '.$value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($method, $path, $query, $headers, $time) + { + $algorithm = "bce-auth-v1"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $this->getCanonicalUri($path); + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $canonicalRequest = $httpRequestMethod."\n" + .$canonicalUri."\n" + .$canonicalQueryString."\n" + .$canonicalHeaders; + + // step 2: calculate signing key + $date = gmdate("Y-m-d\TH:i:s\Z", $time); + $expirationInSeconds = 1800; + $authString = $algorithm . '/' . $this->AccessKeyId . '/' . $date . '/' . $expirationInSeconds; + $signingKey = hash_hmac('sha256', $authString, $this->SecretAccessKey); + + // step 3: sign string + $signature = hash_hmac("sha256", $canonicalRequest, $signingKey); + + // step 4: build authorization + $authorization = $authString . '/' . $signedHeaders . "/" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalUri($path) + { + if (empty($path)) return '/'; + $uri = str_replace('%2F', '/', $this->escape($path)); + if (substr($uri, 0, 1) !== '/') $uri = '/' . $uri; + return $uri; + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + if ($key == 'authorization') continue; + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $this->escape($key) . ':' . $this->escape($value) . "\n"; + $signedHeaders .= $key . ';'; + } + $canonicalHeaders = substr($canonicalHeaders, 0, -1); + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + if ($this->proxy) { + curl_set_proxy($ch); + } + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + $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 (empty($response) && $httpCode == 200) { + return true; + } + $arr = json_decode($response, true); + if ($arr) { + if (isset($arr['code']) && isset($arr['message'])) { + throw new Exception($arr['message']); + } else { + return $arr; + } + } else { + throw new Exception('返回数据解析失败'); + } + } } \ No newline at end of file diff --git a/app/lib/client/Ctyun.php b/app/lib/client/Ctyun.php index 6c92863..59b30f6 100644 --- a/app/lib/client/Ctyun.php +++ b/app/lib/client/Ctyun.php @@ -1,164 +1,164 @@ -AccessKeyId = $AccessKeyId; - $this->SecretAccessKey = $SecretAccessKey; - $this->endpoint = $endpoint; - $this->proxy = $proxy; - } - - /** - * @param string $method 请求方法 - * @param string $path 请求路径 - * @param array|null $query 请求参数 - * @param array|null $params 请求体 - * @return array - * @throws Exception - */ - public function request($method, $path, $query = null, $params = null) - { - if (!empty($query)) { - $query = array_filter($query, function ($a) { return $a !== null;}); - } - if (!empty($params)) { - $params = array_filter($params, function ($a) { return $a !== null;}); - } - - $time = time(); - $date = date("Ymd\THis\Z", $time); - $body = !empty($params) ? json_encode($params) : ''; - $headers = [ - 'Host' => $this->endpoint, - 'Eop-date' => $date, - 'ctyun-eop-request-id' => getSid(), - ]; - if ($body) { - $headers['Content-Type'] = 'application/json'; - } - - $authorization = $this->generateSign($query, $headers, $body, $date); - $headers['Eop-Authorization'] = $authorization; - - $url = 'https://' . $this->endpoint . $path; - if (!empty($query)) { - $url .= '?' . http_build_query($query); - } - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key . ': ' . $value; - } - return $this->curl($method, $url, $body, $header); - } - - private function generateSign($query, $headers, $body, $date) - { - // step 1: build canonical request string - $canonicalQueryString = $this->getCanonicalQueryString($query); - [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); - $hashedRequestPayload = hash("sha256", $body); - - // step 2: build string to sign - $stringToSign = $canonicalHeaders . "\n" - . $canonicalQueryString . "\n" - . $hashedRequestPayload; - - // step 3: sign string - $ktime = hash_hmac("sha256", $date, $this->SecretAccessKey, true); - $kAk = hash_hmac("sha256", $this->AccessKeyId, $ktime, true); - $kdate = hash_hmac("sha256", substr($date, 0, 8), $kAk, true); - $signature = hash_hmac("sha256", $stringToSign, $kdate, true); - $signature = base64_encode($signature); - - // step 4: build authorization - $authorization = $this->AccessKeyId . " Headers=" . $signedHeaders . " Signature=" . $signature; - - return $authorization; - } - - private function escape($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } - - private function getCanonicalQueryString($parameters) - { - if (empty($parameters)) return ''; - ksort($parameters); - $canonicalQueryString = ''; - foreach ($parameters as $key => $value) { - $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); - } - return substr($canonicalQueryString, 1); - } - - private function getCanonicalHeaders($oldheaders) - { - $headers = array(); - foreach ($oldheaders as $key => $value) { - $headers[strtolower($key)] = trim($value); - } - ksort($headers); - - $canonicalHeaders = ''; - $signedHeaders = ''; - foreach ($headers as $key => $value) { - $canonicalHeaders .= $key . ':' . $value . "\n"; - $signedHeaders .= $key . ';'; - } - $signedHeaders = substr($signedHeaders, 0, -1); - return [$canonicalHeaders, $signedHeaders]; - } - - private function curl($method, $url, $body, $header) - { - $ch = curl_init($url); - if ($this->proxy) { - curl_set_proxy($ch); - } - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - if (!empty($body)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - $response = curl_exec($ch); - $errno = curl_errno($ch); - if ($errno) { - $errmsg = curl_error($ch); - curl_close($ch); - throw new Exception('Curl error: ' . $errmsg); - } - curl_close($ch); - - $arr = json_decode($response, true); - if (isset($arr['statusCode']) && $arr['statusCode'] == 100000) { - return isset($arr['returnObj']) ? $arr['returnObj'] : true; - } elseif (isset($arr['errorMessage'])) { - throw new Exception($arr['errorMessage']); - } elseif (isset($arr['message'])) { - throw new Exception($arr['message']); - } else { - throw new Exception('返回数据解析失败'); - } - } -} +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->endpoint = $endpoint; + $this->proxy = $proxy; + } + + /** + * @param string $method 请求方法 + * @param string $path 请求路径 + * @param array|null $query 请求参数 + * @param array|null $params 请求体 + * @return array + * @throws Exception + */ + public function request($method, $path, $query = null, $params = null) + { + if (!empty($query)) { + $query = array_filter($query, function ($a) { return $a !== null;}); + } + if (!empty($params)) { + $params = array_filter($params, function ($a) { return $a !== null;}); + } + + $time = time(); + $date = date("Ymd\THis\Z", $time); + $body = !empty($params) ? json_encode($params) : ''; + $headers = [ + 'Host' => $this->endpoint, + 'Eop-date' => $date, + 'ctyun-eop-request-id' => getSid(), + ]; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + + $authorization = $this->generateSign($query, $headers, $body, $date); + $headers['Eop-Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($query, $headers, $body, $date) + { + // step 1: build canonical request string + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + + // step 2: build string to sign + $stringToSign = $canonicalHeaders . "\n" + . $canonicalQueryString . "\n" + . $hashedRequestPayload; + + // step 3: sign string + $ktime = hash_hmac("sha256", $date, $this->SecretAccessKey, true); + $kAk = hash_hmac("sha256", $this->AccessKeyId, $ktime, true); + $kdate = hash_hmac("sha256", substr($date, 0, 8), $kAk, true); + $signature = hash_hmac("sha256", $stringToSign, $kdate, true); + $signature = base64_encode($signature); + + // step 4: build authorization + $authorization = $this->AccessKeyId . " Headers=" . $signedHeaders . " Signature=" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + if ($this->proxy) { + curl_set_proxy($ch); + } + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + $errmsg = curl_error($ch); + curl_close($ch); + throw new Exception('Curl error: ' . $errmsg); + } + curl_close($ch); + + $arr = json_decode($response, true); + if (isset($arr['statusCode']) && $arr['statusCode'] == 100000) { + return isset($arr['returnObj']) ? $arr['returnObj'] : true; + } elseif (isset($arr['errorMessage'])) { + throw new Exception($arr['errorMessage']); + } elseif (isset($arr['message'])) { + throw new Exception($arr['message']); + } else { + throw new Exception('返回数据解析失败'); + } + } +} diff --git a/app/lib/client/HuaweiCloud.php b/app/lib/client/HuaweiCloud.php index c07aac8..2e8391c 100644 --- a/app/lib/client/HuaweiCloud.php +++ b/app/lib/client/HuaweiCloud.php @@ -1,192 +1,192 @@ -AccessKeyId = $AccessKeyId; - $this->SecretAccessKey = $SecretAccessKey; - $this->endpoint = $endpoint; - $this->proxy = $proxy; - } - - /** - * @param string $method 请求方法 - * @param string $path 请求路径 - * @param array|null $query 请求参数 - * @param array|null $params 请求体 - * @return array - * @throws Exception - */ - public function request($method, $path, $query = null, $params = null) - { - if (!empty($query)) { - $query = array_filter($query, function ($a) { return $a !== null;}); - } - if (!empty($params)) { - $params = array_filter($params, function ($a) { return $a !== null;}); - } - - $time = time(); - $date = gmdate("Ymd\THis\Z", $time); - $body = !empty($params) ? json_encode($params) : ''; - $headers = [ - 'Host' => $this->endpoint, - 'X-Sdk-Date' => $date, - ]; - if ($body) { - $headers['Content-Type'] = 'application/json'; - } - - $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); - $headers['Authorization'] = $authorization; - - $url = 'https://' . $this->endpoint . $path; - if (!empty($query)) { - $url .= '?' . http_build_query($query); - } - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key . ': ' . $value; - } - return $this->curl($method, $url, $body, $header); - } - - private function generateSign($method, $path, $query, $headers, $body, $time) - { - $algorithm = "SDK-HMAC-SHA256"; - - // step 1: build canonical request string - $httpRequestMethod = $method; - $canonicalUri = $this->getCanonicalURI($path); - $canonicalQueryString = $this->getCanonicalQueryString($query); - [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); - $hashedRequestPayload = hash("sha256", $body); - $canonicalRequest = $httpRequestMethod . "\n" - . $canonicalUri . "\n" - . $canonicalQueryString . "\n" - . $canonicalHeaders . "\n" - . $signedHeaders . "\n" - . $hashedRequestPayload; - - // step 2: build string to sign - $date = gmdate("Ymd\THis\Z", $time); - $hashedCanonicalRequest = hash("sha256", $canonicalRequest); - $stringToSign = $algorithm . "\n" - . $date . "\n" - . $hashedCanonicalRequest; - - // step 3: sign string - $signature = hash_hmac("sha256", $stringToSign, $this->SecretAccessKey); - - // step 4: build authorization - $authorization = $algorithm . ' Access=' . $this->AccessKeyId . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; - - return $authorization; - } - - private function escape($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } - - private function getCanonicalURI($path) - { - if (empty($path)) return '/'; - $pattens = explode('/', $path); - $pattens = array_map(function ($item) { - return $this->escape($item); - }, $pattens); - $canonicalURI = implode('/', $pattens); - if (substr($canonicalURI, -1) != '/') $canonicalURI .= '/'; - return $canonicalURI; - } - - private function getCanonicalQueryString($parameters) - { - if (empty($parameters)) return ''; - ksort($parameters); - $canonicalQueryString = ''; - foreach ($parameters as $key => $value) { - $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); - } - return substr($canonicalQueryString, 1); - } - - private function getCanonicalHeaders($oldheaders) - { - $headers = array(); - foreach ($oldheaders as $key => $value) { - $headers[strtolower($key)] = trim($value); - } - ksort($headers); - - $canonicalHeaders = ''; - $signedHeaders = ''; - foreach ($headers as $key => $value) { - $canonicalHeaders .= $key . ':' . $value . "\n"; - $signedHeaders .= $key . ';'; - } - $signedHeaders = substr($signedHeaders, 0, -1); - return [$canonicalHeaders, $signedHeaders]; - } - - private function curl($method, $url, $body, $header) - { - $ch = curl_init($url); - if ($this->proxy) { - curl_set_proxy($ch); - } - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - if (!empty($body)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - $response = curl_exec($ch); - $errno = curl_errno($ch); - if ($errno) { - $errmsg = curl_error($ch); - curl_close($ch); - throw new Exception('Curl error: ' . $errmsg); - } - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - $arr = json_decode($response, true); - if ($arr) { - if (isset($arr['error_msg'])) { - throw new Exception($arr['error_msg']); - } elseif (isset($arr['message'])) { - throw new Exception($arr['message']); - } elseif (isset($arr['error']['error_msg'])) { - throw new Exception($arr['error']['error_msg']); - } else { - return $arr; - } - } else { - if ($httpCode >= 200 && $httpCode < 300) { - return null; - } else { - throw new Exception('返回数据解析失败'); - } - } - } -} +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->endpoint = $endpoint; + $this->proxy = $proxy; + } + + /** + * @param string $method 请求方法 + * @param string $path 请求路径 + * @param array|null $query 请求参数 + * @param array|null $params 请求体 + * @return array + * @throws Exception + */ + public function request($method, $path, $query = null, $params = null) + { + if (!empty($query)) { + $query = array_filter($query, function ($a) { return $a !== null;}); + } + if (!empty($params)) { + $params = array_filter($params, function ($a) { return $a !== null;}); + } + + $time = time(); + $date = gmdate("Ymd\THis\Z", $time); + $body = !empty($params) ? json_encode($params) : ''; + $headers = [ + 'Host' => $this->endpoint, + 'X-Sdk-Date' => $date, + ]; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($method, $path, $query, $headers, $body, $time) + { + $algorithm = "SDK-HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $this->getCanonicalURI($path); + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + $canonicalRequest = $httpRequestMethod . "\n" + . $canonicalUri . "\n" + . $canonicalQueryString . "\n" + . $canonicalHeaders . "\n" + . $signedHeaders . "\n" + . $hashedRequestPayload; + + // step 2: build string to sign + $date = gmdate("Ymd\THis\Z", $time); + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = $algorithm . "\n" + . $date . "\n" + . $hashedCanonicalRequest; + + // step 3: sign string + $signature = hash_hmac("sha256", $stringToSign, $this->SecretAccessKey); + + // step 4: build authorization + $authorization = $algorithm . ' Access=' . $this->AccessKeyId . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalURI($path) + { + if (empty($path)) return '/'; + $pattens = explode('/', $path); + $pattens = array_map(function ($item) { + return $this->escape($item); + }, $pattens); + $canonicalURI = implode('/', $pattens); + if (substr($canonicalURI, -1) != '/') $canonicalURI .= '/'; + return $canonicalURI; + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + if ($this->proxy) { + curl_set_proxy($ch); + } + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + $errmsg = curl_error($ch); + curl_close($ch); + throw new Exception('Curl error: ' . $errmsg); + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $arr = json_decode($response, true); + if ($arr) { + if (isset($arr['error_msg'])) { + throw new Exception($arr['error_msg']); + } elseif (isset($arr['message'])) { + throw new Exception($arr['message']); + } elseif (isset($arr['error']['error_msg'])) { + throw new Exception($arr['error']['error_msg']); + } else { + return $arr; + } + } else { + if ($httpCode >= 200 && $httpCode < 300) { + return null; + } else { + throw new Exception('返回数据解析失败'); + } + } + } +} diff --git a/app/lib/client/Jdcloud.php b/app/lib/client/Jdcloud.php index e47f7bf..d45523e 100644 --- a/app/lib/client/Jdcloud.php +++ b/app/lib/client/Jdcloud.php @@ -1,191 +1,191 @@ -AccessKeyId = $AccessKeyId; - $this->AccessKeySecret = $AccessKeySecret; - $this->endpoint = $endpoint; - $this->service = $service; - $this->region = $region; - $this->proxy = $proxy; - } - - /** - * @param string $method 请求方法 - * @param string $path 请求路径 - * @param array $params 请求参数 - * @return array - * @throws Exception - */ - public function request($method, $path, $params = []) - { - if (!empty($params)) { - $params = array_filter($params, function ($a) { - return $a !== null; - }); - } - - if ($method == 'GET' || $method == 'DELETE') { - $query = $params; - $body = ''; - } else { - $query = []; - $body = !empty($params) ? json_encode($params) : ''; - } - - $date = gmdate("Ymd\THis\Z"); - $headers = [ - 'Host' => $this->endpoint, - 'x-jdcloud-algorithm' => self::$algorithm, - 'x-jdcloud-date' => $date, - 'x-jdcloud-nonce' => uniqid('php', true), - ]; - if ($body) { - $headers['Content-Type'] = 'application/json'; - } - - $authorization = $this->generateSign($method, $path, $query, $headers, $body, $date); - $headers['authorization'] = $authorization; - - $url = 'https://' . $this->endpoint . $path; - if (!empty($query)) { - $url .= '?' . http_build_query($query); - } - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key . ': ' . $value; - } - return $this->curl($method, $url, $body, $header); - } - - private function generateSign($method, $path, $query, $headers, $body, $date) - { - // step 1: build canonical request string - $httpRequestMethod = $method; - $canonicalUri = $path; - $canonicalQueryString = $this->getCanonicalQueryString($query); - [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); - $hashedRequestPayload = hash("sha256", $body); - $canonicalRequest = $httpRequestMethod . "\n" - . $canonicalUri . "\n" - . $canonicalQueryString . "\n" - . $canonicalHeaders . "\n" - . $signedHeaders . "\n" - . $hashedRequestPayload; - - // step 2: build string to sign - $shortDate = substr($date, 0, 8); - $credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/jdcloud2_request'; - $hashedCanonicalRequest = hash("sha256", $canonicalRequest); - $stringToSign = self::$algorithm . "\n" - . $date . "\n" - . $credentialScope . "\n" - . $hashedCanonicalRequest; - - // step 3: sign string - $kDate = hash_hmac("sha256", $shortDate, 'JDCLOUD2' . $this->AccessKeySecret, true); - $kRegion = hash_hmac("sha256", $this->region, $kDate, true); - $kService = hash_hmac("sha256", $this->service, $kRegion, true); - $kSigning = hash_hmac("sha256", "jdcloud2_request", $kService, true); - $signature = hash_hmac("sha256", $stringToSign, $kSigning); - - // step 4: build authorization - $credential = $this->AccessKeyId . '/' . $credentialScope; - $authorization = self::$algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; - - return $authorization; - } - - private function escape($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } - - private function getCanonicalQueryString($parameters) - { - if (empty($parameters)) return ''; - ksort($parameters); - $canonicalQueryString = ''; - foreach ($parameters as $key => $value) { - $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); - } - return substr($canonicalQueryString, 1); - } - - private function getCanonicalHeaders($oldheaders) - { - $headers = array(); - foreach ($oldheaders as $key => $value) { - $headers[strtolower($key)] = trim($value); - } - ksort($headers); - - $canonicalHeaders = ''; - $signedHeaders = ''; - foreach ($headers as $key => $value) { - $canonicalHeaders .= $key . ':' . $value . "\n"; - $signedHeaders .= $key . ';'; - } - $signedHeaders = substr($signedHeaders, 0, -1); - return [$canonicalHeaders, $signedHeaders]; - } - - private function curl($method, $url, $body, $header) - { - $ch = curl_init($url); - if ($this->proxy) { - curl_set_proxy($ch); - } - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - if (!empty($body)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - $response = curl_exec($ch); - $errno = curl_errno($ch); - if ($errno) { - $errmsg = curl_error($ch); - curl_close($ch); - throw new Exception('Curl error: ' . $errmsg); - } - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - $arr = json_decode($response, true); - if ($httpCode == 200) { - if (isset($arr['result'])) { - return $arr['result']; - } - return $arr; - } else { - if (isset($arr['error']['message'])) { - throw new Exception($arr['error']['message']); - } else { - throw new Exception('返回数据解析失败(http_code=' . $httpCode . ')'); - } - } - } -} +AccessKeyId = $AccessKeyId; + $this->AccessKeySecret = $AccessKeySecret; + $this->endpoint = $endpoint; + $this->service = $service; + $this->region = $region; + $this->proxy = $proxy; + } + + /** + * @param string $method 请求方法 + * @param string $path 请求路径 + * @param array $params 请求参数 + * @return array + * @throws Exception + */ + public function request($method, $path, $params = []) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + if ($method == 'GET' || $method == 'DELETE') { + $query = $params; + $body = ''; + } else { + $query = []; + $body = !empty($params) ? json_encode($params) : ''; + } + + $date = gmdate("Ymd\THis\Z"); + $headers = [ + 'Host' => $this->endpoint, + 'x-jdcloud-algorithm' => self::$algorithm, + 'x-jdcloud-date' => $date, + 'x-jdcloud-nonce' => uniqid('php', true), + ]; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $date); + $headers['authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($method, $path, $query, $headers, $body, $date) + { + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $path; + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + $canonicalRequest = $httpRequestMethod . "\n" + . $canonicalUri . "\n" + . $canonicalQueryString . "\n" + . $canonicalHeaders . "\n" + . $signedHeaders . "\n" + . $hashedRequestPayload; + + // step 2: build string to sign + $shortDate = substr($date, 0, 8); + $credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/jdcloud2_request'; + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = self::$algorithm . "\n" + . $date . "\n" + . $credentialScope . "\n" + . $hashedCanonicalRequest; + + // step 3: sign string + $kDate = hash_hmac("sha256", $shortDate, 'JDCLOUD2' . $this->AccessKeySecret, true); + $kRegion = hash_hmac("sha256", $this->region, $kDate, true); + $kService = hash_hmac("sha256", $this->service, $kRegion, true); + $kSigning = hash_hmac("sha256", "jdcloud2_request", $kService, true); + $signature = hash_hmac("sha256", $stringToSign, $kSigning); + + // step 4: build authorization + $credential = $this->AccessKeyId . '/' . $credentialScope; + $authorization = self::$algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + if ($this->proxy) { + curl_set_proxy($ch); + } + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + $errmsg = curl_error($ch); + curl_close($ch); + throw new Exception('Curl error: ' . $errmsg); + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $arr = json_decode($response, true); + if ($httpCode == 200) { + if (isset($arr['result'])) { + return $arr['result']; + } + return $arr; + } else { + if (isset($arr['error']['message'])) { + throw new Exception($arr['error']['message']); + } else { + throw new Exception('返回数据解析失败(http_code=' . $httpCode . ')'); + } + } + } +} diff --git a/app/lib/client/Ksyun.php b/app/lib/client/Ksyun.php index 4cea059..a1c5341 100644 --- a/app/lib/client/Ksyun.php +++ b/app/lib/client/Ksyun.php @@ -1,209 +1,209 @@ -AccessKeyId = $AccessKeyId; - $this->SecretAccessKey = $SecretAccessKey; - $this->endpoint = $endpoint; - $this->service = $service; - $this->region = $region; - $this->proxy = $proxy; - } - - /** - * @param string $method 请求方法 - * @param string $action 方法名称 - * @param array $params 请求参数 - * @return array - * @throws Exception - */ - public function request($method, $action, $version, $path = '/', $params = []) - { - if (!empty($params)) { - $params = array_filter($params, function ($a) { - return $a !== null; - }); - } - - $body = ''; - $query = []; - if ($method == 'GET') { - $query = $params; - } else { - $body = !empty($params) ? json_encode($params) : ''; - } - - $time = time(); - $headers = [ - 'Host' => $this->endpoint, - 'X-Amz-Date' => gmdate("Ymd\THis\Z", $time), - 'X-Version' => $version, - 'X-Action' => $action, - ]; - - $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); - $headers['Authorization'] = $authorization; - $headers['Accept'] = 'application/json'; - if ($body) { - $headers['Content-Type'] = 'application/json'; - } - - $url = 'https://' . $this->endpoint . $path; - if (!empty($query)) { - $url .= '?' . http_build_query($query); - } - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key . ': ' . $value; - } - return $this->curl($method, $url, $body, $header); - } - - private function generateSign($method, $path, $query, $headers, $body, $time) - { - $algorithm = "AWS4-HMAC-SHA256"; - - // step 1: build canonical request string - $httpRequestMethod = $method; - $canonicalUri = $this->getCanonicalURI($path); - $canonicalQueryString = $this->getCanonicalQueryString($query); - [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); - $hashedRequestPayload = hash("sha256", $body); - $canonicalRequest = $httpRequestMethod . "\n" - . $canonicalUri . "\n" - . $canonicalQueryString . "\n" - . $canonicalHeaders . "\n" - . $signedHeaders . "\n" - . $hashedRequestPayload; - - // step 2: build string to sign - $date = gmdate("Ymd\THis\Z", $time); - $shortDate = substr($date, 0, 8); - $credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/aws4_request'; - $hashedCanonicalRequest = hash("sha256", $canonicalRequest); - $stringToSign = $algorithm . "\n" - . $date . "\n" - . $credentialScope . "\n" - . $hashedCanonicalRequest; - - // step 3: sign string - $kDate = hash_hmac("sha256", $shortDate, 'AWS4' . $this->SecretAccessKey, true); - $kRegion = hash_hmac("sha256", $this->region, $kDate, true); - $kService = hash_hmac("sha256", $this->service, $kRegion, true); - $kSigning = hash_hmac("sha256", "aws4_request", $kService, true); - $signature = hash_hmac("sha256", $stringToSign, $kSigning); - - // step 4: build authorization - $credential = $this->AccessKeyId . '/' . $credentialScope; - $authorization = $algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; - - return $authorization; - } - - private function escape($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } - - private function getCanonicalURI($path) - { - if (empty($path)) return '/'; - $pattens = explode('/', $path); - $pattens = array_map(function ($item) { - return $this->escape($item); - }, $pattens); - $canonicalURI = implode('/', $pattens); - return $canonicalURI; - } - - private function getCanonicalQueryString($parameters) - { - if (empty($parameters)) return ''; - ksort($parameters); - $canonicalQueryString = ''; - foreach ($parameters as $key => $value) { - if (!is_array($value)) { - $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); - } else { - sort($value); - foreach ($value as $v) { - $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($v); - } - } - } - return substr($canonicalQueryString, 1); - } - - private function getCanonicalHeaders($oldheaders) - { - $headers = array(); - foreach ($oldheaders as $key => $value) { - $headers[strtolower($key)] = trim($value); - } - ksort($headers); - - $canonicalHeaders = ''; - $signedHeaders = ''; - foreach ($headers as $key => $value) { - $canonicalHeaders .= $key . ':' . $value . "\n"; - $signedHeaders .= $key . ';'; - } - $signedHeaders = substr($signedHeaders, 0, -1); - return [$canonicalHeaders, $signedHeaders]; - } - - private function curl($method, $url, $body, $header) - { - $ch = curl_init($url); - if ($this->proxy) { - curl_set_proxy($ch); - } - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - if (!empty($body)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - $response = curl_exec($ch); - $errno = curl_errno($ch); - if ($errno) { - $errmsg = curl_error($ch); - curl_close($ch); - throw new Exception('Curl error: ' . $errmsg); - } - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - $arr = json_decode($response, true); - if ($httpCode == 200) { - return $arr; - } else { - if (isset($arr['Error']['Message'])) { - throw new Exception($arr['Error']['Message']); - } else { - throw new Exception('返回数据解析失败(http_code=' . $httpCode . ')'); - } - } - } -} +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->endpoint = $endpoint; + $this->service = $service; + $this->region = $region; + $this->proxy = $proxy; + } + + /** + * @param string $method 请求方法 + * @param string $action 方法名称 + * @param array $params 请求参数 + * @return array + * @throws Exception + */ + public function request($method, $action, $version, $path = '/', $params = []) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + $body = ''; + $query = []; + if ($method == 'GET') { + $query = $params; + } else { + $body = !empty($params) ? json_encode($params) : ''; + } + + $time = time(); + $headers = [ + 'Host' => $this->endpoint, + 'X-Amz-Date' => gmdate("Ymd\THis\Z", $time), + 'X-Version' => $version, + 'X-Action' => $action, + ]; + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); + $headers['Authorization'] = $authorization; + $headers['Accept'] = 'application/json'; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($method, $path, $query, $headers, $body, $time) + { + $algorithm = "AWS4-HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $this->getCanonicalURI($path); + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + $canonicalRequest = $httpRequestMethod . "\n" + . $canonicalUri . "\n" + . $canonicalQueryString . "\n" + . $canonicalHeaders . "\n" + . $signedHeaders . "\n" + . $hashedRequestPayload; + + // step 2: build string to sign + $date = gmdate("Ymd\THis\Z", $time); + $shortDate = substr($date, 0, 8); + $credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/aws4_request'; + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = $algorithm . "\n" + . $date . "\n" + . $credentialScope . "\n" + . $hashedCanonicalRequest; + + // step 3: sign string + $kDate = hash_hmac("sha256", $shortDate, 'AWS4' . $this->SecretAccessKey, true); + $kRegion = hash_hmac("sha256", $this->region, $kDate, true); + $kService = hash_hmac("sha256", $this->service, $kRegion, true); + $kSigning = hash_hmac("sha256", "aws4_request", $kService, true); + $signature = hash_hmac("sha256", $stringToSign, $kSigning); + + // step 4: build authorization + $credential = $this->AccessKeyId . '/' . $credentialScope; + $authorization = $algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalURI($path) + { + if (empty($path)) return '/'; + $pattens = explode('/', $path); + $pattens = array_map(function ($item) { + return $this->escape($item); + }, $pattens); + $canonicalURI = implode('/', $pattens); + return $canonicalURI; + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + if (!is_array($value)) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } else { + sort($value); + foreach ($value as $v) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($v); + } + } + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + if ($this->proxy) { + curl_set_proxy($ch); + } + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + $errmsg = curl_error($ch); + curl_close($ch); + throw new Exception('Curl error: ' . $errmsg); + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $arr = json_decode($response, true); + if ($httpCode == 200) { + return $arr; + } else { + if (isset($arr['Error']['Message'])) { + throw new Exception($arr['Error']['Message']); + } else { + throw new Exception('返回数据解析失败(http_code=' . $httpCode . ')'); + } + } + } +} diff --git a/app/lib/client/Qiniu.php b/app/lib/client/Qiniu.php index b9a3f4d..a9096e0 100644 --- a/app/lib/client/Qiniu.php +++ b/app/lib/client/Qiniu.php @@ -1,143 +1,143 @@ -AccessKey = $AccessKey; - $this->SecretKey = $SecretKey; - $this->proxy = $proxy; - } - - /** - * @param string $method 请求方法 - * @param string $path 请求路径 - * @param array|null $query 请求参数 - * @param array|null $params 请求体 - * @return array - * @throws Exception - */ - public function request($method, $path, $query = null, $params = null) - { - $url = $this->ApiUrl . $path; - $query_str = null; - $body = null; - if (!empty($query)) { - $query = array_filter($query, function ($a) { - return $a !== null; - }); - $query_str = http_build_query($query); - $url .= '?' . $query_str; - } - if (!empty($params)) { - $params = array_filter($params, function ($a) { - return $a !== null; - }); - $body = json_encode($params); - } - - $sign_str = $path . ($query_str ? '?' . $query_str : '') . "\n"; - $hmac = hash_hmac('sha1', $sign_str, $this->SecretKey, true); - $sign = $this->AccessKey . ':' . $this->base64_urlSafeEncode($hmac); - - $header = [ - 'Authorization: QBox ' . $sign, - ]; - if ($body) { - $header[] = 'Content-Type: application/json'; - } - return $this->curl($method, $url, $body, $header); - } - - public function pili_request($method, $path, $query = null, $params = null) - { - $this->ApiUrl = 'https://pili.qiniuapi.com'; - $url = $this->ApiUrl . $path; - $query_str = null; - $body = null; - if (!empty($query)) { - $query = array_filter($query, function ($a) { - return $a !== null; - }); - $query_str = http_build_query($query); - $url .= '?' . $query_str; - } - if (!empty($params)) { - $params = array_filter($params, function ($a) { - return $a !== null; - }); - $body = json_encode($params); - } - - $sign_str = $method . ' ' . $path . ($query_str ? '?' . $query_str : '') . "\nHost: pili.qiniuapi.com" . ($body ? "\nContent-Type: application/json" : '') . "\n\n" . $body; - $hmac = hash_hmac('sha1', $sign_str, $this->SecretKey, true); - $sign = $this->AccessKey . ':' . $this->base64_urlSafeEncode($hmac); - - $header = [ - 'Authorization: Qiniu ' . $sign, - ]; - if ($body) { - $header[] = 'Content-Type: application/json'; - } - return $this->curl($method, $url, $body, $header); - } - - private function base64_urlSafeEncode($data) - { - $find = array('+', '/'); - $replace = array('-', '_'); - return str_replace($find, $replace, base64_encode($data)); - } - - 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_USERAGENT, 'QiniuPHP/7.14.0 (' . php_uname("s") . '/' . php_uname("m") . ') PHP/' . phpversion()); - 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) { - $arr = json_decode($response, true); - if ($arr) return $arr; - return true; - } else { - $arr = json_decode($response, true); - if ($arr && !empty($arr['error'])) { - throw new Exception($arr['error']); - } else { - throw new Exception('返回数据解析失败'); - } - } - } -} +AccessKey = $AccessKey; + $this->SecretKey = $SecretKey; + $this->proxy = $proxy; + } + + /** + * @param string $method 请求方法 + * @param string $path 请求路径 + * @param array|null $query 请求参数 + * @param array|null $params 请求体 + * @return array + * @throws Exception + */ + public function request($method, $path, $query = null, $params = null) + { + $url = $this->ApiUrl . $path; + $query_str = null; + $body = null; + if (!empty($query)) { + $query = array_filter($query, function ($a) { + return $a !== null; + }); + $query_str = http_build_query($query); + $url .= '?' . $query_str; + } + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + $body = json_encode($params); + } + + $sign_str = $path . ($query_str ? '?' . $query_str : '') . "\n"; + $hmac = hash_hmac('sha1', $sign_str, $this->SecretKey, true); + $sign = $this->AccessKey . ':' . $this->base64_urlSafeEncode($hmac); + + $header = [ + 'Authorization: QBox ' . $sign, + ]; + if ($body) { + $header[] = 'Content-Type: application/json'; + } + return $this->curl($method, $url, $body, $header); + } + + public function pili_request($method, $path, $query = null, $params = null) + { + $this->ApiUrl = 'https://pili.qiniuapi.com'; + $url = $this->ApiUrl . $path; + $query_str = null; + $body = null; + if (!empty($query)) { + $query = array_filter($query, function ($a) { + return $a !== null; + }); + $query_str = http_build_query($query); + $url .= '?' . $query_str; + } + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + $body = json_encode($params); + } + + $sign_str = $method . ' ' . $path . ($query_str ? '?' . $query_str : '') . "\nHost: pili.qiniuapi.com" . ($body ? "\nContent-Type: application/json" : '') . "\n\n" . $body; + $hmac = hash_hmac('sha1', $sign_str, $this->SecretKey, true); + $sign = $this->AccessKey . ':' . $this->base64_urlSafeEncode($hmac); + + $header = [ + 'Authorization: Qiniu ' . $sign, + ]; + if ($body) { + $header[] = 'Content-Type: application/json'; + } + return $this->curl($method, $url, $body, $header); + } + + private function base64_urlSafeEncode($data) + { + $find = array('+', '/'); + $replace = array('-', '_'); + return str_replace($find, $replace, base64_encode($data)); + } + + 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_USERAGENT, 'QiniuPHP/7.14.0 (' . php_uname("s") . '/' . php_uname("m") . ') PHP/' . phpversion()); + 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) { + $arr = json_decode($response, true); + if ($arr) return $arr; + return true; + } else { + $arr = json_decode($response, true); + if ($arr && !empty($arr['error'])) { + throw new Exception($arr['error']); + } else { + throw new Exception('返回数据解析失败'); + } + } + } +} diff --git a/app/lib/client/TencentCloud.php b/app/lib/client/TencentCloud.php index 3cb6d04..6571f54 100644 --- a/app/lib/client/TencentCloud.php +++ b/app/lib/client/TencentCloud.php @@ -1,133 +1,133 @@ -SecretId = $SecretId; - $this->SecretKey = $SecretKey; - $this->endpoint = $endpoint; - $this->service = $service; - $this->version = $version; - $this->region = $region; - $this->proxy = $proxy; - } - - /** - * @param string $action 方法名称 - * @param array $param 请求参数 - * @return array - * @throws Exception - */ - public function request($action, $param) - { - $param = array_filter($param, function ($a) { return $a !== null;}); - if (!$param) $param = (object)[]; - $payload = json_encode($param); - $time = time(); - $authorization = $this->generateSign($payload, $time); - $header = [ - 'Authorization: '.$authorization, - 'Content-Type: application/json; charset=utf-8', - 'X-TC-Action: '.$action, - 'X-TC-Timestamp: '.$time, - 'X-TC-Version: '.$this->version, - ]; - if($this->region) { - $header[] = 'X-TC-Region: '.$this->region; - } - $res = $this->curl_post($payload, $header); - return $res; - } - - private function generateSign($payload, $time) - { - $algorithm = "TC3-HMAC-SHA256"; - - // step 1: build canonical request string - $httpRequestMethod = "POST"; - $canonicalUri = "/"; - $canonicalQueryString = ""; - $canonicalHeaders = "content-type:application/json; charset=utf-8\n"."host:".$this->endpoint."\n"; - $signedHeaders = "content-type;host"; - $hashedRequestPayload = hash("SHA256", $payload); - $canonicalRequest = $httpRequestMethod."\n" - .$canonicalUri."\n" - .$canonicalQueryString."\n" - .$canonicalHeaders."\n" - .$signedHeaders."\n" - .$hashedRequestPayload; - - // step 2: build string to sign - $date = gmdate("Y-m-d", $time); - $credentialScope = $date."/".$this->service."/tc3_request"; - $hashedCanonicalRequest = hash("SHA256", $canonicalRequest); - $stringToSign = $algorithm."\n" - .$time."\n" - .$credentialScope."\n" - .$hashedCanonicalRequest; - - // step 3: sign string - $secretDate = hash_hmac("SHA256", $date, "TC3".$this->SecretKey, true); - $secretService = hash_hmac("SHA256", $this->service, $secretDate, true); - $secretSigning = hash_hmac("SHA256", "tc3_request", $secretService, true); - $signature = hash_hmac("SHA256", $stringToSign, $secretSigning); - - // step 4: build authorization - $authorization = $algorithm - ." Credential=".$this->SecretId."/".$credentialScope - .", SignedHeaders=content-type;host, Signature=".$signature; - - return $authorization; - } - - private function curl_post($payload, $header) - { - $url = 'https://'.$this->endpoint.'/'; - $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_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); - $response = curl_exec($ch); - $errno = curl_errno($ch); - if ($errno) { - $errmsg = curl_error($ch); - curl_close($ch); - throw new Exception('Curl error: ' . $errmsg); - } - curl_close($ch); - - $arr = json_decode($response, true); - if ($arr) { - if (isset($arr['Response']['Error'])) { - throw new Exception($arr['Response']['Error']['Message']); - } else { - return $arr['Response']; - } - } else { - throw new Exception('返回数据解析失败'); - } - } +SecretId = $SecretId; + $this->SecretKey = $SecretKey; + $this->endpoint = $endpoint; + $this->service = $service; + $this->version = $version; + $this->region = $region; + $this->proxy = $proxy; + } + + /** + * @param string $action 方法名称 + * @param array $param 请求参数 + * @return array + * @throws Exception + */ + public function request($action, $param) + { + $param = array_filter($param, function ($a) { return $a !== null;}); + if (!$param) $param = (object)[]; + $payload = json_encode($param); + $time = time(); + $authorization = $this->generateSign($payload, $time); + $header = [ + 'Authorization: '.$authorization, + 'Content-Type: application/json; charset=utf-8', + 'X-TC-Action: '.$action, + 'X-TC-Timestamp: '.$time, + 'X-TC-Version: '.$this->version, + ]; + if($this->region) { + $header[] = 'X-TC-Region: '.$this->region; + } + $res = $this->curl_post($payload, $header); + return $res; + } + + private function generateSign($payload, $time) + { + $algorithm = "TC3-HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = "POST"; + $canonicalUri = "/"; + $canonicalQueryString = ""; + $canonicalHeaders = "content-type:application/json; charset=utf-8\n"."host:".$this->endpoint."\n"; + $signedHeaders = "content-type;host"; + $hashedRequestPayload = hash("SHA256", $payload); + $canonicalRequest = $httpRequestMethod."\n" + .$canonicalUri."\n" + .$canonicalQueryString."\n" + .$canonicalHeaders."\n" + .$signedHeaders."\n" + .$hashedRequestPayload; + + // step 2: build string to sign + $date = gmdate("Y-m-d", $time); + $credentialScope = $date."/".$this->service."/tc3_request"; + $hashedCanonicalRequest = hash("SHA256", $canonicalRequest); + $stringToSign = $algorithm."\n" + .$time."\n" + .$credentialScope."\n" + .$hashedCanonicalRequest; + + // step 3: sign string + $secretDate = hash_hmac("SHA256", $date, "TC3".$this->SecretKey, true); + $secretService = hash_hmac("SHA256", $this->service, $secretDate, true); + $secretSigning = hash_hmac("SHA256", "tc3_request", $secretService, true); + $signature = hash_hmac("SHA256", $stringToSign, $secretSigning); + + // step 4: build authorization + $authorization = $algorithm + ." Credential=".$this->SecretId."/".$credentialScope + .", SignedHeaders=content-type;host, Signature=".$signature; + + return $authorization; + } + + private function curl_post($payload, $header) + { + $url = 'https://'.$this->endpoint.'/'; + $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_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + $errmsg = curl_error($ch); + curl_close($ch); + throw new Exception('Curl error: ' . $errmsg); + } + curl_close($ch); + + $arr = json_decode($response, true); + if ($arr) { + if (isset($arr['Response']['Error'])) { + throw new Exception($arr['Response']['Error']['Message']); + } else { + return $arr['Response']; + } + } else { + throw new Exception('返回数据解析失败'); + } + } } \ No newline at end of file diff --git a/app/lib/client/Ucloud.php b/app/lib/client/Ucloud.php index b2bf886..9b16751 100644 --- a/app/lib/client/Ucloud.php +++ b/app/lib/client/Ucloud.php @@ -1,51 +1,51 @@ -PublicKey = $PublicKey; - $this->PrivateKey = $PrivateKey; - } - - public function request($action, $params) - { - $param = [ - 'Action' => $action, - 'PublicKey' => $this->PublicKey, - ]; - $param = array_merge($param, $params); - $param['Signature'] = $this->ucloudSignature($param); - $ua = sprintf("PHP/%s PHP-SDK/%s", phpversion(), self::VERSION); - $response = get_curl($this->ApiUrl, json_encode($param), 0, 0, $ua, 0, ['Content-Type' => 'application/json']); - $result = json_decode($response, true); - if (isset($result['RetCode']) && $result['RetCode'] == 0) { - return $result; - } elseif (isset($result['Message'])) { - throw new Exception($result['Message']); - } else { - throw new Exception('返回数据解析失败'); - } - } - - private function ucloudSignature($param) - { - ksort($param); - $str = ''; - foreach ($param as $key => $value) { - $str .= $key . $value; - } - $str .= $this->PrivateKey; - return sha1($str); - } -} +PublicKey = $PublicKey; + $this->PrivateKey = $PrivateKey; + } + + public function request($action, $params) + { + $param = [ + 'Action' => $action, + 'PublicKey' => $this->PublicKey, + ]; + $param = array_merge($param, $params); + $param['Signature'] = $this->ucloudSignature($param); + $ua = sprintf("PHP/%s PHP-SDK/%s", phpversion(), self::VERSION); + $response = get_curl($this->ApiUrl, json_encode($param), 0, 0, $ua, 0, ['Content-Type' => 'application/json']); + $result = json_decode($response, true); + if (isset($result['RetCode']) && $result['RetCode'] == 0) { + return $result; + } elseif (isset($result['Message'])) { + throw new Exception($result['Message']); + } else { + throw new Exception('返回数据解析失败'); + } + } + + private function ucloudSignature($param) + { + ksort($param); + $str = ''; + foreach ($param as $key => $value) { + $str .= $key . $value; + } + $str .= $this->PrivateKey; + return sha1($str); + } +} diff --git a/app/lib/client/Volcengine.php b/app/lib/client/Volcengine.php index e5a975a..25df322 100644 --- a/app/lib/client/Volcengine.php +++ b/app/lib/client/Volcengine.php @@ -1,252 +1,252 @@ -AccessKeyId = $AccessKeyId; - $this->SecretAccessKey = $SecretAccessKey; - $this->endpoint = $endpoint; - $this->service = $service; - $this->version = $version; - $this->region = $region; - $this->proxy = $proxy; - } - - /** - * @param string $method 请求方法 - * @param string $action 方法名称 - * @param array $params 请求参数 - * @return array - * @throws Exception - */ - public function request($method, $action, $params = [], $querys = []) - { - if (!empty($params)) { - $params = array_filter($params, function ($a) { - return $a !== null; - }); - } - - $query = [ - 'Action' => $action, - 'Version' => $this->version, - ]; - - $body = ''; - if ($method == 'GET') { - $query = array_merge($query, $params); - } else { - $body = !empty($params) ? json_encode($params) : ''; - if (!empty($querys)) { - $query = array_merge($query, $querys); - } - } - - $time = time(); - $headers = [ - 'Host' => $this->endpoint, - 'X-Date' => gmdate("Ymd\THis\Z", $time), - //'X-Content-Sha256' => hash("sha256", $body), - ]; - if ($body) { - $headers['Content-Type'] = 'application/json'; - } - $path = '/'; - - $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); - $headers['Authorization'] = $authorization; - - $url = 'https://' . $this->endpoint . $path . '?' . http_build_query($query); - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key . ': ' . $value; - } - return $this->curl($method, $url, $body, $header); - } - - /** - * @param string $method 请求方法 - * @param string $action 方法名称 - * @param array $params 请求参数 - * @return array - * @throws Exception - */ - public function tos_request($method, $params = [], $query = []) - { - if (!empty($params)) { - $params = array_filter($params, function ($a) { - return $a !== null; - }); - } - - $body = ''; - if ($method != 'GET') { - $body = !empty($params) ? json_encode($params) : ''; - } - - $time = time(); - $headers = [ - 'Host' => $this->endpoint, - 'X-Tos-Date' => gmdate("Ymd\THis\Z", $time), - 'X-Tos-Content-Sha256' => hash("sha256", $body), - ]; - if ($body) { - $headers['Content-Type'] = 'application/json'; - } - $path = '/'; - - $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); - $headers['Authorization'] = $authorization; - - $url = 'https://' . $this->endpoint . $path . '?' . http_build_query($query); - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key . ': ' . $value; - } - return $this->curl($method, $url, $body, $header); - } - - private function generateSign($method, $path, $query, $headers, $body, $time) - { - $algorithm = $this->service == 'tos' ? "TOS4-HMAC-SHA256" : "HMAC-SHA256"; - - // step 1: build canonical request string - $httpRequestMethod = $method; - $canonicalUri = $path; - if (substr($canonicalUri, -1) != "/") $canonicalUri .= "/"; - $canonicalQueryString = $this->getCanonicalQueryString($query); - [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); - $hashedRequestPayload = hash("sha256", $body); - $canonicalRequest = $httpRequestMethod . "\n" - . $canonicalUri . "\n" - . $canonicalQueryString . "\n" - . $canonicalHeaders . "\n" - . $signedHeaders . "\n" - . $hashedRequestPayload; - - // step 2: build string to sign - $date = gmdate("Ymd\THis\Z", $time); - $shortDate = substr($date, 0, 8); - $credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/request'; - $hashedCanonicalRequest = hash("sha256", $canonicalRequest); - $stringToSign = $algorithm . "\n" - . $date . "\n" - . $credentialScope . "\n" - . $hashedCanonicalRequest; - - // step 3: sign string - $kDate = hash_hmac("sha256", $shortDate, $this->SecretAccessKey, true); - $kRegion = hash_hmac("sha256", $this->region, $kDate, true); - $kService = hash_hmac("sha256", $this->service, $kRegion, true); - $kSigning = hash_hmac("sha256", "request", $kService, true); - $signature = hash_hmac("sha256", $stringToSign, $kSigning); - - // step 4: build authorization - $credential = $this->AccessKeyId . '/' . $credentialScope; - $authorization = $algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; - - return $authorization; - } - - private function escape($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } - - private function getCanonicalQueryString($parameters) - { - if (empty($parameters)) return ''; - ksort($parameters); - $canonicalQueryString = ''; - foreach ($parameters as $key => $value) { - $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); - } - return substr($canonicalQueryString, 1); - } - - private function getCanonicalHeaders($oldheaders) - { - $headers = array(); - foreach ($oldheaders as $key => $value) { - $headers[strtolower($key)] = trim($value); - } - ksort($headers); - - $canonicalHeaders = ''; - $signedHeaders = ''; - foreach ($headers as $key => $value) { - $canonicalHeaders .= $key . ':' . $value . "\n"; - $signedHeaders .= $key . ';'; - } - $signedHeaders = substr($signedHeaders, 0, -1); - return [$canonicalHeaders, $signedHeaders]; - } - - private function curl($method, $url, $body, $header) - { - $ch = curl_init($url); - if ($this->proxy) { - curl_set_proxy($ch); - } - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - if (!empty($body)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - $response = curl_exec($ch); - $errno = curl_errno($ch); - if ($errno) { - $errmsg = curl_error($ch); - curl_close($ch); - throw new Exception('Curl error: ' . $errmsg); - } - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - $arr = json_decode($response, true); - if ($httpCode == 200) { - if (isset($arr['ResponseMetadata']['Error']['MessageCN'])) { - throw new Exception($arr['ResponseMetadata']['Error']['MessageCN']); - } elseif (isset($arr['ResponseMetadata']['Error']['Message'])) { - throw new Exception($arr['ResponseMetadata']['Error']['Message']); - } elseif (isset($arr['Result'])) { - return $arr['Result']; - } - return true; - } else { - if (isset($arr['ResponseMetadata']['Error']['MessageCN'])) { - throw new Exception($arr['ResponseMetadata']['Error']['MessageCN']); - } elseif (isset($arr['ResponseMetadata']['Error']['Message'])) { - throw new Exception($arr['ResponseMetadata']['Error']['Message']); - } elseif (isset($arr['Message'])) { - throw new Exception($arr['Message']); - } elseif (isset($arr['message'])) { - throw new Exception($arr['message']); - } else { - throw new Exception('返回数据解析失败(http_code=' . $httpCode . ')'); - } - } - } -} +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->endpoint = $endpoint; + $this->service = $service; + $this->version = $version; + $this->region = $region; + $this->proxy = $proxy; + } + + /** + * @param string $method 请求方法 + * @param string $action 方法名称 + * @param array $params 请求参数 + * @return array + * @throws Exception + */ + public function request($method, $action, $params = [], $querys = []) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + $query = [ + 'Action' => $action, + 'Version' => $this->version, + ]; + + $body = ''; + if ($method == 'GET') { + $query = array_merge($query, $params); + } else { + $body = !empty($params) ? json_encode($params) : ''; + if (!empty($querys)) { + $query = array_merge($query, $querys); + } + } + + $time = time(); + $headers = [ + 'Host' => $this->endpoint, + 'X-Date' => gmdate("Ymd\THis\Z", $time), + //'X-Content-Sha256' => hash("sha256", $body), + ]; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + $path = '/'; + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path . '?' . http_build_query($query); + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + /** + * @param string $method 请求方法 + * @param string $action 方法名称 + * @param array $params 请求参数 + * @return array + * @throws Exception + */ + public function tos_request($method, $params = [], $query = []) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + $body = ''; + if ($method != 'GET') { + $body = !empty($params) ? json_encode($params) : ''; + } + + $time = time(); + $headers = [ + 'Host' => $this->endpoint, + 'X-Tos-Date' => gmdate("Ymd\THis\Z", $time), + 'X-Tos-Content-Sha256' => hash("sha256", $body), + ]; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + $path = '/'; + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path . '?' . http_build_query($query); + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($method, $path, $query, $headers, $body, $time) + { + $algorithm = $this->service == 'tos' ? "TOS4-HMAC-SHA256" : "HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $path; + if (substr($canonicalUri, -1) != "/") $canonicalUri .= "/"; + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + $canonicalRequest = $httpRequestMethod . "\n" + . $canonicalUri . "\n" + . $canonicalQueryString . "\n" + . $canonicalHeaders . "\n" + . $signedHeaders . "\n" + . $hashedRequestPayload; + + // step 2: build string to sign + $date = gmdate("Ymd\THis\Z", $time); + $shortDate = substr($date, 0, 8); + $credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/request'; + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = $algorithm . "\n" + . $date . "\n" + . $credentialScope . "\n" + . $hashedCanonicalRequest; + + // step 3: sign string + $kDate = hash_hmac("sha256", $shortDate, $this->SecretAccessKey, true); + $kRegion = hash_hmac("sha256", $this->region, $kDate, true); + $kService = hash_hmac("sha256", $this->service, $kRegion, true); + $kSigning = hash_hmac("sha256", "request", $kService, true); + $signature = hash_hmac("sha256", $stringToSign, $kSigning); + + // step 4: build authorization + $credential = $this->AccessKeyId . '/' . $credentialScope; + $authorization = $algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + if ($this->proxy) { + curl_set_proxy($ch); + } + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + $errmsg = curl_error($ch); + curl_close($ch); + throw new Exception('Curl error: ' . $errmsg); + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $arr = json_decode($response, true); + if ($httpCode == 200) { + if (isset($arr['ResponseMetadata']['Error']['MessageCN'])) { + throw new Exception($arr['ResponseMetadata']['Error']['MessageCN']); + } elseif (isset($arr['ResponseMetadata']['Error']['Message'])) { + throw new Exception($arr['ResponseMetadata']['Error']['Message']); + } elseif (isset($arr['Result'])) { + return $arr['Result']; + } + return true; + } else { + if (isset($arr['ResponseMetadata']['Error']['MessageCN'])) { + throw new Exception($arr['ResponseMetadata']['Error']['MessageCN']); + } elseif (isset($arr['ResponseMetadata']['Error']['Message'])) { + throw new Exception($arr['ResponseMetadata']['Error']['Message']); + } elseif (isset($arr['Message'])) { + throw new Exception($arr['Message']); + } elseif (isset($arr['message'])) { + throw new Exception($arr['message']); + } else { + throw new Exception('返回数据解析失败(http_code=' . $httpCode . ')'); + } + } + } +} diff --git a/app/lib/deploy/aliyun.php b/app/lib/deploy/aliyun.php index 3bf768a..b0ed2c5 100644 --- a/app/lib/deploy/aliyun.php +++ b/app/lib/deploy/aliyun.php @@ -1,735 +1,735 @@ -AccessKeyId = $config['AccessKeyId']; - $this->AccessKeySecret = $config['AccessKeySecret']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - } - - public function check() - { - if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) throw new Exception('必填参数不能为空'); - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'cas.aliyuncs.com', '2020-04-07', $this->proxy); - $param = ['Action' => 'ListUserCertificateOrder']; - $client->request($param); - return true; - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if ($config['product'] == 'api') { - $this->deploy_api($fullchain, $privatekey, $config); - } elseif ($config['product'] == 'vod') { - $this->deploy_vod($fullchain, $privatekey, $config); - } elseif ($config['product'] == 'fc') { - $this->deploy_fc($fullchain, $privatekey, $config); - } elseif ($config['product'] == 'fc2') { - $this->deploy_fc2($fullchain, $privatekey, $config); - } else { - [$cert_id, $cert_name] = $this->get_cert_id($fullchain, $privatekey, $config); - if (!$cert_id) throw new Exception('证书ID获取失败'); - if ($config['product'] == 'cdn') { - $this->deploy_cdn($cert_id, $cert_name, $config); - } elseif ($config['product'] == 'dcdn') { - $this->deploy_dcdn($cert_id, $cert_name, $config); - } elseif ($config['product'] == 'esa') { - $this->deploy_esa($cert_id, $cert_name, $config); - } elseif ($config['product'] == 'oss') { - $this->deploy_oss($cert_id, $config); - } elseif ($config['product'] == 'waf') { - $this->deploy_waf($cert_id, $config); - } elseif ($config['product'] == 'waf2') { - $this->deploy_waf2($cert_id, $config); - } elseif ($config['product'] == 'ddoscoo') { - $this->deploy_ddoscoo($cert_id, $config); - } elseif ($config['product'] == 'live') { - $this->deploy_live($cert_id, $cert_name, $config); - } elseif ($config['product'] == 'clb') { - $this->deploy_clb($cert_id, $cert_name, $config); - } elseif ($config['product'] == 'alb') { - $this->deploy_alb($cert_id, $config); - } elseif ($config['product'] == 'nlb') { - $this->deploy_nlb($cert_id, $config); - } else { - throw new Exception('未知的产品类型'); - } - $info['cert_id'] = $cert_id; - $info['cert_name'] = $cert_name; - } - } - - private function get_cert_id($fullchain, $privatekey, $config) - { - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - $serial_no = strtolower($certInfo['serialNumberHex']); - - if ($config['region'] == 'ap-southeast-1') { - $endpoint = 'cas.ap-southeast-1.aliyuncs.com'; - } else { - $endpoint = 'cas.aliyuncs.com'; - } - - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2020-04-07', $this->proxy); - $param = [ - 'Action' => 'ListUserCertificateOrder', - 'Keyword' => $certInfo['subject']['CN'], - 'OrderType' => 'CERT', - ]; - try { - $data = $client->request($param); - } catch (Exception $e) { - throw new Exception('查询证书列表失败:' . $e->getMessage()); - } - $cert_id = null; - if ($data['TotalCount'] > 0 && !empty($data['CertificateOrderList'])) { - foreach ($data['CertificateOrderList'] as $cert) { - if (strtolower($cert['SerialNo']) == $serial_no || strpos(strtolower($cert['SerialNo']), $serial_no) !== false) { - $cert_id = $cert['CertificateId']; - $cert_name = $cert['Name']; - break; - } - } - } - if ($cert_id) { - $this->log('找到已上传的证书 CertId=' . $cert_id); - return [$cert_id, $cert_name]; - } - - $param = [ - 'Action' => 'UploadUserCertificate', - 'Name' => $cert_name, - 'Cert' => $fullchain, - 'Key' => $privatekey, - ]; - try { - $data = $client->request($param); - } catch (Exception $e) { - throw new Exception('上传证书失败:' . $e->getMessage()); - } - $this->log('证书上传成功!CertId=' . $data['CertId']); - usleep(500000); - return [$data['CertId'], $cert_name]; - } - - private function deploy_cdn($cert_id, $cert_name, $config) - { - $domain = $config['domain']; - if (empty($domain)) throw new Exception('CDN绑定域名不能为空'); - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'cdn.aliyuncs.com', '2018-05-10', $this->proxy); - $param = [ - 'Action' => 'SetCdnDomainSSLCertificate', - 'DomainName' => $domain, - 'CertName' => $cert_name, - 'CertType' => 'cas', - 'SSLProtocol' => 'on', - 'CertId' => $cert_id, - ]; - $client->request($param); - $this->log('CDN域名 ' . $domain . ' 部署证书成功!'); - } - - private function deploy_dcdn($cert_id, $cert_name, $config) - { - $domain = $config['domain']; - if (empty($domain)) throw new Exception('DCDN绑定域名不能为空'); - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'dcdn.aliyuncs.com', '2018-01-15', $this->proxy); - $param = [ - 'Action' => 'SetDcdnDomainSSLCertificate', - 'DomainName' => $domain, - 'CertName' => $cert_name, - 'CertType' => 'cas', - 'SSLProtocol' => 'on', - 'CertId' => $cert_id, - ]; - $client->request($param); - $this->log('DCDN域名 ' . $domain . ' 部署证书成功!'); - } - - private function deploy_esa($cas_id, $cert_name, $config) - { - $sitename = $config['esa_sitename']; - if (empty($sitename)) throw new Exception('ESA站点名称不能为空'); - - if ($config['region'] == 'ap-southeast-1') { - $endpoint = 'esa.ap-southeast-1.aliyuncs.com'; - } else { - $endpoint = 'esa.cn-hangzhou.aliyuncs.com'; - } - - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2024-09-10'); - $param = [ - 'Action' => 'ListSites', - 'SiteName' => $sitename, - 'SiteSearchType' => 'exact', - ]; - try { - $data = $client->request($param, 'GET'); - } catch (Exception $e) { - throw new Exception('查询ESA站点列表失败:' . $e->getMessage()); - } - if ($data['TotalCount'] == 0) throw new Exception('ESA站点 ' . $sitename . ' 不存在'); - $this->log('成功查询到' . $data['TotalCount'] . '个ESA站点'); - $site_id = $data['Sites'][0]['SiteId']; - - $param = [ - 'Action' => 'ListCertificates', - 'SiteId' => $site_id, - ]; - try { - $data = $client->request($param, 'GET'); - } catch (Exception $e) { - throw new Exception('查询ESA站点' . $sitename . '证书列表失败:' . $e->getMessage()); - } - $this->log('ESA站点 ' . $sitename . ' 查询到' . $data['TotalCount'] . '个SSL证书'); - - $exist_cert_id = null; - $exist_cert_name = null; - $exist_cert_casid = null; - if ($data['TotalCount'] > 0) { - foreach ($data['Result'] as $cert) { - $domains = explode(',', $cert['SAN']); - $flag = true; - foreach ($domains as $domain) { - if (!in_array($domain, $config['domainList'])) { - $flag = false; - break; - } - } - if ($flag) { - $exist_cert_id = $cert['Id']; - $exist_cert_name = $cert['Name']; - $exist_cert_casid = isset($cert['CasId']) ? $cert['CasId'] : null; - break; - } - } - } - - $param = [ - 'Action' => 'SetCertificate', - 'SiteId' => $site_id, - 'Type' => 'cas', - 'CasId' => $cas_id, - 'Name' => $cert_name, - 'Region' => $config['region'], - ]; - - if ($exist_cert_id) { - $param['Id'] = $exist_cert_id; - - if ($exist_cert_casid == $cas_id) { - $this->log('ESA站点 ' . $sitename . ' 证书已配置,无需重复操作'); - return; - } - } - - $client->request($param); - - if ($exist_cert_name) { - $this->log('ESA站点 ' . $sitename . ' 证书 ' . $exist_cert_name . ' 更新成功'); - } else { - $this->log('ESA站点 ' . $sitename . ' 证书添加成功!'); - } - } - - private function deploy_oss($cert_id, $config) - { - if (empty($config['domain'])) throw new Exception('OSS绑定域名不能为空'); - if (empty($config['oss_endpoint'])) throw new Exception('OSS Endpoint不能为空'); - if (empty($config['oss_bucket'])) throw new Exception('OSS Bucket不能为空'); - $client = new AliyunOSS($this->AccessKeyId, $this->AccessKeySecret, $config['oss_endpoint']); - $client->addBucketCnameCert($config['oss_bucket'], $config['domain'], $cert_id . '-cn-hangzhou'); - $this->log('OSS域名 ' . $config['domain'] . ' 部署证书成功!'); - } - - private function deploy_waf($cert_id, $config) - { - $domain = $config['domain']; - if (empty($domain)) throw new Exception('WAF绑定域名不能为空'); - - if ($config['region'] == 'ap-southeast-1') { - $cert_id .= '-ap-southeast-1'; - } else { - $cert_id .= '-cn-hangzhou'; - } - - $endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com'; - - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2021-10-01', $this->proxy); - - $param = [ - 'Action' => 'DescribeInstance', - 'RegionId' => $config['region'], - ]; - try { - $data = $client->request($param, 'GET'); - } catch (Exception $e) { - throw new Exception('获取WAF实例详情失败:' . $e->getMessage()); - } - if (empty($data['InstanceId'])) throw new Exception('当前账号未找到WAF实例'); - $instance_id = $data['InstanceId']; - $this->log('获取WAF实例ID成功 InstanceId=' . $instance_id); - - $param = [ - 'Action' => 'DescribeDomainDetail', - 'InstanceId' => $instance_id, - 'Domain' => $domain, - 'RegionId' => $config['region'], - ]; - try { - $data = $client->request($param, 'GET'); - } catch (Exception $e) { - throw new Exception('查询CNAME接入详情失败:' . $e->getMessage()); - } - if (!isset($data['Listen'])) { - throw new Exception('没有找到' . $domain . '监听器'); - } - - if (isset($data['Listen']['CertId'])) { - $old_cert_id = $data['Listen']['CertId']; - if (!empty($old_cert_id) && $old_cert_id == $cert_id) { - $this->log('WAF域名 ' . $domain . ' 证书已配置,无需重复操作'); - return; - } - } - - $data['Listen']['CertId'] = $cert_id; - if (empty($data['Listen']['HttpsPorts'])) $data['Listen']['HttpsPorts'] = [443]; - $data['Redirect']['Backends'] = $data['Redirect']['AllBackends']; - $param = [ - 'Action' => 'ModifyDomain', - 'InstanceId' => $instance_id, - 'Domain' => $domain, - 'Listen' => json_encode($data['Listen']), - 'Redirect' => json_encode($data['Redirect']), - 'RegionId' => $config['region'], - ]; - $data = $client->request($param); - - $this->log('WAF域名 ' . $domain . ' 部署证书成功!'); - } - - private function deploy_waf2($cert_id, $config) - { - $domain = $config['domain']; - if (empty($domain)) throw new Exception('WAF绑定域名不能为空'); - - $endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com'; - - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2019-09-10', $this->proxy); - - $param = [ - 'Action' => 'DescribeInstanceInfo', - 'RegionId' => $config['region'], - ]; - try { - $data = $client->request($param, 'GET'); - } catch (Exception $e) { - throw new Exception('获取WAF实例详情失败:' . $e->getMessage()); - } - if (empty($data['InstanceInfo']['InstanceId'])) throw new Exception('当前账号未找到WAF实例'); - $instance_id = $data['InstanceInfo']['InstanceId']; - $this->log('获取WAF实例ID成功 InstanceId=' . $instance_id); - - $param = [ - 'Action' => 'CreateCertificateByCertificateId', - 'InstanceId' => $instance_id, - 'Domain' => $domain, - 'CertificateId' => $cert_id, - ]; - $client->request($param); - - $this->log('WAF域名 ' . $domain . ' 部署证书成功!'); - } - - private function deploy_api($fullchain, $privatekey, $config) - { - $domain = $config['domain']; - $groupid = $config['api_groupid']; - if (empty($groupid)) throw new Exception('API分组ID不能为空'); - if (empty($domain)) throw new Exception('API分组绑定域名不能为空'); - - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $endpoint = 'apigateway.' . $config['regionid'] . '.aliyuncs.com'; - - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2016-07-14', $this->proxy); - - $param = [ - 'Action' => 'SetDomainCertificate', - 'GroupId' => $groupid, - 'DomainName' => $domain, - 'CertificateName' => $cert_name, - 'CertificateBody' => $fullchain, - 'CertificatePrivateKey' => $privatekey, - ]; - $client->request($param); - - $this->log('API网关域名 ' . $domain . ' 部署证书成功!'); - } - - private function deploy_ddoscoo($cert_id, $config) - { - $domain = $config['domain']; - if (empty($domain)) throw new Exception('绑定域名不能为空'); - - $endpoint = 'ddoscoo.' . $config['region'] . '.aliyuncs.com'; - - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2020-01-01', $this->proxy); - - $param = [ - 'Action' => 'AssociateWebCert', - 'Domain' => $domain, - 'CertId' => $cert_id, - ]; - $client->request($param); - - $this->log('DDoS高防域名 ' . $domain . ' 部署证书成功!'); - } - - private function deploy_live($cert_id, $cert_name, $config) - { - $domain = $config['domain']; - if (empty($domain)) throw new Exception('视频直播绑定域名不能为空'); - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'live.aliyuncs.com', '2016-11-01', $this->proxy); - $param = [ - 'Action' => 'SetLiveDomainCertificate', - 'DomainName' => $domain, - 'CertName' => $cert_name, - 'CertType' => 'cas', - 'SSLProtocol' => 'on', - 'CertId' => $cert_id, - ]; - $client->request($param); - $this->log('设置视频直播域名 ' . $domain . ' 证书成功!'); - } - - private function deploy_vod($fullchain, $privatekey, $config) - { - $domain = $config['domain']; - if (empty($domain)) throw new Exception('视频点播绑定域名不能为空'); - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'vod.cn-shanghai.aliyuncs.com', '2017-03-21', $this->proxy); - $param = [ - 'Action' => 'SetVodDomainCertificate', - 'DomainName' => $domain, - 'SSLProtocol' => 'on', - 'SSLPub' => $fullchain, - 'SSLPri' => $privatekey, - ]; - $client->request($param); - $this->log('视频点播域名 ' . $domain . ' 部署证书成功!'); - } - - private function deploy_fc($fullchain, $privatekey, $config) - { - $domain = $config['domain']; - $fc_cname = $config['fc_cname']; - if (empty($domain)) throw new Exception('函数计算域名不能为空'); - if (empty($fc_cname)) throw new Exception('域名CNAME地址不能为空'); - - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $client = new AliyunNewClient($this->AccessKeyId, $this->AccessKeySecret, $fc_cname, '2023-03-30', $this->proxy); - - try { - $data = $client->request('GET', 'GetCustomDomain', '/2023-03-30/custom-domains/' . $domain); - } catch (Exception $e) { - throw new Exception('获取绑定域名信息失败:' . $e->getMessage()); - } - $this->log('获取函数计算绑定域名信息成功'); - - if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) { - $this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作'); - return; - } - - if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS'; - $data['certConfig']['certName'] = $cert_name; - $data['certConfig']['certificate'] = $fullchain; - $data['certConfig']['privateKey'] = $privatekey; - - $param = [ - 'authConfig' => $data['authConfig'], - 'certConfig' => $data['certConfig'], - 'protocol' => $data['protocol'], - 'routeConfig' => $data['routeConfig'], - 'tlsConfig' => $data['tlsConfig'], - 'wafConfig' => $data['wafConfig'], - ]; - $client->request('PUT', 'UpdateCustomDomain', '/2023-03-30/custom-domains/' . $domain, $param); - - $this->log('函数计算域名 ' . $domain . ' 部署证书成功!'); - } - - private function deploy_fc2($fullchain, $privatekey, $config) - { - $domain = $config['domain']; - $fc_cname = $config['fc_cname']; - if (empty($domain)) throw new Exception('函数计算域名不能为空'); - if (empty($fc_cname)) throw new Exception('域名CNAME地址不能为空'); - - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $client = new AliyunNewClient($this->AccessKeyId, $this->AccessKeySecret, $fc_cname, '2021-04-06', $this->proxy); - - try { - $data = $client->request('GET', 'GetCustomDomain', '/2021-04-06/custom-domains/' . $domain); - } catch (Exception $e) { - throw new Exception('获取绑定域名信息失败:' . $e->getMessage()); - } - $this->log('获取函数计算绑定域名信息成功'); - - if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) { - $this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作'); - return; - } - - if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS'; - $data['certConfig']['certName'] = $cert_name; - $data['certConfig']['certificate'] = $fullchain; - $data['certConfig']['privateKey'] = $privatekey; - - $param = [ - 'protocol' => $data['protocol'], - 'routeConfig' => $data['routeConfig'], - 'certConfig' => $data['certConfig'], - 'tlsConfig' => $data['tlsConfig'], - 'wafConfig' => $data['wafConfig'], - ]; - $client->request('PUT', 'UpdateCustomDomain', '/2021-04-06/custom-domains/' . $domain, $param); - - $this->log('函数计算域名 ' . $domain . ' 部署证书成功!'); - } - - private function deploy_clb($cert_id, $cert_name, $config) - { - if (empty($config['clb_id'])) throw new Exception('负载均衡实例ID不能为空'); - if (empty($config['clb_port'])) throw new Exception('HTTPS监听端口不能为空'); - - $endpoint = 'slb.' . $config['regionid'] . '.aliyuncs.com'; - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2014-05-15', $this->proxy); - - $param = [ - 'Action' => 'DescribeServerCertificates', - 'RegionId' => $config['regionid'], - ]; - try { - $data = $client->request($param); - } catch (Exception $e) { - throw new Exception('获取服务器证书列表失败:' . $e->getMessage()); - } - - $ServerCertificateId = null; - foreach ($data['ServerCertificates']['ServerCertificate'] as $cert) { - if ($cert['IsAliCloudCertificate'] == 1 && $cert['AliCloudCertificateId'] == $cert_id) { - $ServerCertificateId = $cert['ServerCertificateId']; - break; - } - } - if (!$ServerCertificateId) { - $param = [ - 'Action' => 'UploadServerCertificate', - 'RegionId' => $config['regionid'], - 'AliCloudCertificateId' => $cert_id, - 'AliCloudCertificateName' => $cert_name, - 'AliCloudCertificateRegionId' => 'cn-hangzhou', - ]; - try { - $data = $client->request($param); - } catch (Exception $e) { - throw new Exception('服务器证书添加失败:' . $e->getMessage()); - } - $ServerCertificateId = $data['ServerCertificateId']; - $this->log('服务器证书添加成功 ServerCertificateId=' . $ServerCertificateId); - } else { - $this->log('找到已添加的服务器证书 ServerCertificateId=' . $ServerCertificateId); - } - - $deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0; - if ($deploy_type == 1) { - if (empty($config['clb_domain'])) throw new Exception('扩展域名不能为空'); - $domains = explode(',', $config['clb_domain']); - $param = [ - 'Action' => 'DescribeDomainExtensions', - 'RegionId' => $config['regionid'], - 'LoadBalancerId' => $config['clb_id'], - 'ListenerPort' => $config['clb_port'], - ]; - try { - $data = $client->request($param); - } catch (Exception $e) { - throw new Exception('扩展域名列表查询失败:' . $e->getMessage()); - } - foreach ($data['DomainExtensions']['DomainExtension'] as $item) { - if (in_array($item['Domain'], $domains)) { - if ($ServerCertificateId == $item['ServerCertificateId']) { - $this->log('负载均衡HTTPS扩展域名 ' . $item['Domain'] . ' 证书已配置'); - } else { - $param = [ - 'Action' => 'SetDomainExtensionAttribute', - 'RegionId' => $config['regionid'], - 'DomainExtensionId' => $item['DomainExtensionId'], - 'ServerCertificateId' => $ServerCertificateId, - ]; - $client->request($param); - $this->log('负载均衡HTTPS扩展域名 ' . $item['Domain'] . ' 证书更新成功'); - } - } - } - } else { - $param = [ - 'Action' => 'DescribeLoadBalancerHTTPSListenerAttribute', - 'RegionId' => $config['regionid'], - 'LoadBalancerId' => $config['clb_id'], - 'ListenerPort' => $config['clb_port'], - ]; - try { - $data = $client->request($param); - } catch (Exception $e) { - throw new Exception('HTTPS监听配置查询失败:' . $e->getMessage()); - } - - if ($data['ServerCertificateId'] == $ServerCertificateId) { - $this->log('负载均衡HTTPS监听已配置该证书,无需重复操作'); - return; - } - - $param = [ - 'Action' => 'SetLoadBalancerHTTPSListenerAttribute', - 'RegionId' => $config['regionid'], - 'LoadBalancerId' => $config['clb_id'], - 'ListenerPort' => $config['clb_port'], - 'ServerCertificateId' => $ServerCertificateId, - ]; - $client->request($param); - $this->log('负载均衡HTTPS监听证书配置成功!'); - } - } - - private function deploy_alb($cert_id, $config) - { - if (empty($config['alb_listener_id'])) throw new Exception('负载均衡监听ID不能为空'); - - $endpoint = 'alb.' . $config['regionid'] . '.aliyuncs.com'; - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2020-06-16', $this->proxy); - $cert_id = $cert_id . '-cn-hangzhou'; - $deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0; - - if ($deploy_type == 1) { - $param = [ - 'Action' => 'ListListenerCertificates', - 'MaxResults' => 100, - 'ListenerId' => $config['alb_listener_id'], - 'CertificateType' => 'Server', - ]; - try { - $data = $client->request($param); - } catch (Exception $e) { - throw new Exception('获取监听证书列表失败:' . $e->getMessage()); - } - foreach ($data['Certificates'] as $cert) { - if ($cert['CertificateId'] == $cert_id) { - $this->log('负载均衡监听扩展证书已添加,无需重复操作'); - return; - } - } - - $param = [ - 'Action' => 'AssociateAdditionalCertificatesWithListener', - 'ListenerId' => $config['alb_listener_id'], - 'Certificates.1.CertificateId' => $cert_id, - ]; - $client->request($param); - $this->log('应用型负载均衡监听扩展证书添加成功!'); - } else { - $param = [ - 'Action' => 'UpdateListenerAttribute', - 'ListenerId' => $config['alb_listener_id'], - 'Certificates.1.CertificateId' => $cert_id, - ]; - $client->request($param); - $this->log('应用型负载均衡监听默认证书更新成功!'); - } - } - - private function deploy_nlb($cert_id, $config) - { - if (empty($config['nlb_listener_id'])) throw new Exception('负载均衡监听ID不能为空'); - - $endpoint = 'nlb.' . $config['regionid'] . '.aliyuncs.com'; - $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2022-04-30', $this->proxy); - $cert_id = $cert_id . '-cn-hangzhou'; - $deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0; - - if ($deploy_type == 1) { - $param = [ - 'Action' => 'ListListenerCertificates', - 'MaxResults' => 50, - 'ListenerId' => $config['nlb_listener_id'], - 'CertificateType' => 'Server', - ]; - try { - $data = $client->request($param); - } catch (Exception $e) { - throw new Exception('获取监听证书列表失败:' . $e->getMessage()); - } - foreach ($data['Certificates'] as $cert) { - if ($cert['CertificateId'] == $cert_id) { - $this->log('负载均衡监听扩展证书已添加,无需重复操作'); - return; - } - } - - $param = [ - 'Action' => 'AssociateAdditionalCertificatesWithListener', - 'ListenerId' => $config['nlb_listener_id'], - 'AdditionalCertificateIds.1' => $cert_id, - ]; - $client->request($param); - $this->log('网络型负载均衡监听扩展证书添加成功!'); - } else { - $param = [ - 'Action' => 'UpdateListenerAttribute', - 'ListenerId' => $config['nlb_listener_id'], - 'CertificateIds.1' => $cert_id, - ]; - $client->request($param); - $this->log('网络型负载均衡监听默认证书更新成功!'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +AccessKeyId = $config['AccessKeyId']; + $this->AccessKeySecret = $config['AccessKeySecret']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) throw new Exception('必填参数不能为空'); + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'cas.aliyuncs.com', '2020-04-07', $this->proxy); + $param = ['Action' => 'ListUserCertificateOrder']; + $client->request($param); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if ($config['product'] == 'api') { + $this->deploy_api($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'vod') { + $this->deploy_vod($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'fc') { + $this->deploy_fc($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'fc2') { + $this->deploy_fc2($fullchain, $privatekey, $config); + } else { + [$cert_id, $cert_name] = $this->get_cert_id($fullchain, $privatekey, $config); + if (!$cert_id) throw new Exception('证书ID获取失败'); + if ($config['product'] == 'cdn') { + $this->deploy_cdn($cert_id, $cert_name, $config); + } elseif ($config['product'] == 'dcdn') { + $this->deploy_dcdn($cert_id, $cert_name, $config); + } elseif ($config['product'] == 'esa') { + $this->deploy_esa($cert_id, $cert_name, $config); + } elseif ($config['product'] == 'oss') { + $this->deploy_oss($cert_id, $config); + } elseif ($config['product'] == 'waf') { + $this->deploy_waf($cert_id, $config); + } elseif ($config['product'] == 'waf2') { + $this->deploy_waf2($cert_id, $config); + } elseif ($config['product'] == 'ddoscoo') { + $this->deploy_ddoscoo($cert_id, $config); + } elseif ($config['product'] == 'live') { + $this->deploy_live($cert_id, $cert_name, $config); + } elseif ($config['product'] == 'clb') { + $this->deploy_clb($cert_id, $cert_name, $config); + } elseif ($config['product'] == 'alb') { + $this->deploy_alb($cert_id, $config); + } elseif ($config['product'] == 'nlb') { + $this->deploy_nlb($cert_id, $config); + } else { + throw new Exception('未知的产品类型'); + } + $info['cert_id'] = $cert_id; + $info['cert_name'] = $cert_name; + } + } + + private function get_cert_id($fullchain, $privatekey, $config) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + $serial_no = strtolower($certInfo['serialNumberHex']); + + if ($config['region'] == 'ap-southeast-1') { + $endpoint = 'cas.ap-southeast-1.aliyuncs.com'; + } else { + $endpoint = 'cas.aliyuncs.com'; + } + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2020-04-07', $this->proxy); + $param = [ + 'Action' => 'ListUserCertificateOrder', + 'Keyword' => $certInfo['subject']['CN'], + 'OrderType' => 'CERT', + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('查询证书列表失败:' . $e->getMessage()); + } + $cert_id = null; + if ($data['TotalCount'] > 0 && !empty($data['CertificateOrderList'])) { + foreach ($data['CertificateOrderList'] as $cert) { + if (strtolower($cert['SerialNo']) == $serial_no || strpos(strtolower($cert['SerialNo']), $serial_no) !== false) { + $cert_id = $cert['CertificateId']; + $cert_name = $cert['Name']; + break; + } + } + } + if ($cert_id) { + $this->log('找到已上传的证书 CertId=' . $cert_id); + return [$cert_id, $cert_name]; + } + + $param = [ + 'Action' => 'UploadUserCertificate', + 'Name' => $cert_name, + 'Cert' => $fullchain, + 'Key' => $privatekey, + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $this->log('证书上传成功!CertId=' . $data['CertId']); + usleep(500000); + return [$data['CertId'], $cert_name]; + } + + private function deploy_cdn($cert_id, $cert_name, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('CDN绑定域名不能为空'); + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'cdn.aliyuncs.com', '2018-05-10', $this->proxy); + $param = [ + 'Action' => 'SetCdnDomainSSLCertificate', + 'DomainName' => $domain, + 'CertName' => $cert_name, + 'CertType' => 'cas', + 'SSLProtocol' => 'on', + 'CertId' => $cert_id, + ]; + $client->request($param); + $this->log('CDN域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_dcdn($cert_id, $cert_name, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('DCDN绑定域名不能为空'); + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'dcdn.aliyuncs.com', '2018-01-15', $this->proxy); + $param = [ + 'Action' => 'SetDcdnDomainSSLCertificate', + 'DomainName' => $domain, + 'CertName' => $cert_name, + 'CertType' => 'cas', + 'SSLProtocol' => 'on', + 'CertId' => $cert_id, + ]; + $client->request($param); + $this->log('DCDN域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_esa($cas_id, $cert_name, $config) + { + $sitename = $config['esa_sitename']; + if (empty($sitename)) throw new Exception('ESA站点名称不能为空'); + + if ($config['region'] == 'ap-southeast-1') { + $endpoint = 'esa.ap-southeast-1.aliyuncs.com'; + } else { + $endpoint = 'esa.cn-hangzhou.aliyuncs.com'; + } + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2024-09-10'); + $param = [ + 'Action' => 'ListSites', + 'SiteName' => $sitename, + 'SiteSearchType' => 'exact', + ]; + try { + $data = $client->request($param, 'GET'); + } catch (Exception $e) { + throw new Exception('查询ESA站点列表失败:' . $e->getMessage()); + } + if ($data['TotalCount'] == 0) throw new Exception('ESA站点 ' . $sitename . ' 不存在'); + $this->log('成功查询到' . $data['TotalCount'] . '个ESA站点'); + $site_id = $data['Sites'][0]['SiteId']; + + $param = [ + 'Action' => 'ListCertificates', + 'SiteId' => $site_id, + ]; + try { + $data = $client->request($param, 'GET'); + } catch (Exception $e) { + throw new Exception('查询ESA站点' . $sitename . '证书列表失败:' . $e->getMessage()); + } + $this->log('ESA站点 ' . $sitename . ' 查询到' . $data['TotalCount'] . '个SSL证书'); + + $exist_cert_id = null; + $exist_cert_name = null; + $exist_cert_casid = null; + if ($data['TotalCount'] > 0) { + foreach ($data['Result'] as $cert) { + $domains = explode(',', $cert['SAN']); + $flag = true; + foreach ($domains as $domain) { + if (!in_array($domain, $config['domainList'])) { + $flag = false; + break; + } + } + if ($flag) { + $exist_cert_id = $cert['Id']; + $exist_cert_name = $cert['Name']; + $exist_cert_casid = isset($cert['CasId']) ? $cert['CasId'] : null; + break; + } + } + } + + $param = [ + 'Action' => 'SetCertificate', + 'SiteId' => $site_id, + 'Type' => 'cas', + 'CasId' => $cas_id, + 'Name' => $cert_name, + 'Region' => $config['region'], + ]; + + if ($exist_cert_id) { + $param['Id'] = $exist_cert_id; + + if ($exist_cert_casid == $cas_id) { + $this->log('ESA站点 ' . $sitename . ' 证书已配置,无需重复操作'); + return; + } + } + + $client->request($param); + + if ($exist_cert_name) { + $this->log('ESA站点 ' . $sitename . ' 证书 ' . $exist_cert_name . ' 更新成功'); + } else { + $this->log('ESA站点 ' . $sitename . ' 证书添加成功!'); + } + } + + private function deploy_oss($cert_id, $config) + { + if (empty($config['domain'])) throw new Exception('OSS绑定域名不能为空'); + if (empty($config['oss_endpoint'])) throw new Exception('OSS Endpoint不能为空'); + if (empty($config['oss_bucket'])) throw new Exception('OSS Bucket不能为空'); + $client = new AliyunOSS($this->AccessKeyId, $this->AccessKeySecret, $config['oss_endpoint']); + $client->addBucketCnameCert($config['oss_bucket'], $config['domain'], $cert_id . '-cn-hangzhou'); + $this->log('OSS域名 ' . $config['domain'] . ' 部署证书成功!'); + } + + private function deploy_waf($cert_id, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('WAF绑定域名不能为空'); + + if ($config['region'] == 'ap-southeast-1') { + $cert_id .= '-ap-southeast-1'; + } else { + $cert_id .= '-cn-hangzhou'; + } + + $endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com'; + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2021-10-01', $this->proxy); + + $param = [ + 'Action' => 'DescribeInstance', + 'RegionId' => $config['region'], + ]; + try { + $data = $client->request($param, 'GET'); + } catch (Exception $e) { + throw new Exception('获取WAF实例详情失败:' . $e->getMessage()); + } + if (empty($data['InstanceId'])) throw new Exception('当前账号未找到WAF实例'); + $instance_id = $data['InstanceId']; + $this->log('获取WAF实例ID成功 InstanceId=' . $instance_id); + + $param = [ + 'Action' => 'DescribeDomainDetail', + 'InstanceId' => $instance_id, + 'Domain' => $domain, + 'RegionId' => $config['region'], + ]; + try { + $data = $client->request($param, 'GET'); + } catch (Exception $e) { + throw new Exception('查询CNAME接入详情失败:' . $e->getMessage()); + } + if (!isset($data['Listen'])) { + throw new Exception('没有找到' . $domain . '监听器'); + } + + if (isset($data['Listen']['CertId'])) { + $old_cert_id = $data['Listen']['CertId']; + if (!empty($old_cert_id) && $old_cert_id == $cert_id) { + $this->log('WAF域名 ' . $domain . ' 证书已配置,无需重复操作'); + return; + } + } + + $data['Listen']['CertId'] = $cert_id; + if (empty($data['Listen']['HttpsPorts'])) $data['Listen']['HttpsPorts'] = [443]; + $data['Redirect']['Backends'] = $data['Redirect']['AllBackends']; + $param = [ + 'Action' => 'ModifyDomain', + 'InstanceId' => $instance_id, + 'Domain' => $domain, + 'Listen' => json_encode($data['Listen']), + 'Redirect' => json_encode($data['Redirect']), + 'RegionId' => $config['region'], + ]; + $data = $client->request($param); + + $this->log('WAF域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_waf2($cert_id, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('WAF绑定域名不能为空'); + + $endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com'; + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2019-09-10', $this->proxy); + + $param = [ + 'Action' => 'DescribeInstanceInfo', + 'RegionId' => $config['region'], + ]; + try { + $data = $client->request($param, 'GET'); + } catch (Exception $e) { + throw new Exception('获取WAF实例详情失败:' . $e->getMessage()); + } + if (empty($data['InstanceInfo']['InstanceId'])) throw new Exception('当前账号未找到WAF实例'); + $instance_id = $data['InstanceInfo']['InstanceId']; + $this->log('获取WAF实例ID成功 InstanceId=' . $instance_id); + + $param = [ + 'Action' => 'CreateCertificateByCertificateId', + 'InstanceId' => $instance_id, + 'Domain' => $domain, + 'CertificateId' => $cert_id, + ]; + $client->request($param); + + $this->log('WAF域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_api($fullchain, $privatekey, $config) + { + $domain = $config['domain']; + $groupid = $config['api_groupid']; + if (empty($groupid)) throw new Exception('API分组ID不能为空'); + if (empty($domain)) throw new Exception('API分组绑定域名不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $endpoint = 'apigateway.' . $config['regionid'] . '.aliyuncs.com'; + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2016-07-14', $this->proxy); + + $param = [ + 'Action' => 'SetDomainCertificate', + 'GroupId' => $groupid, + 'DomainName' => $domain, + 'CertificateName' => $cert_name, + 'CertificateBody' => $fullchain, + 'CertificatePrivateKey' => $privatekey, + ]; + $client->request($param); + + $this->log('API网关域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_ddoscoo($cert_id, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('绑定域名不能为空'); + + $endpoint = 'ddoscoo.' . $config['region'] . '.aliyuncs.com'; + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2020-01-01', $this->proxy); + + $param = [ + 'Action' => 'AssociateWebCert', + 'Domain' => $domain, + 'CertId' => $cert_id, + ]; + $client->request($param); + + $this->log('DDoS高防域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_live($cert_id, $cert_name, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('视频直播绑定域名不能为空'); + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'live.aliyuncs.com', '2016-11-01', $this->proxy); + $param = [ + 'Action' => 'SetLiveDomainCertificate', + 'DomainName' => $domain, + 'CertName' => $cert_name, + 'CertType' => 'cas', + 'SSLProtocol' => 'on', + 'CertId' => $cert_id, + ]; + $client->request($param); + $this->log('设置视频直播域名 ' . $domain . ' 证书成功!'); + } + + private function deploy_vod($fullchain, $privatekey, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('视频点播绑定域名不能为空'); + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'vod.cn-shanghai.aliyuncs.com', '2017-03-21', $this->proxy); + $param = [ + 'Action' => 'SetVodDomainCertificate', + 'DomainName' => $domain, + 'SSLProtocol' => 'on', + 'SSLPub' => $fullchain, + 'SSLPri' => $privatekey, + ]; + $client->request($param); + $this->log('视频点播域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_fc($fullchain, $privatekey, $config) + { + $domain = $config['domain']; + $fc_cname = $config['fc_cname']; + if (empty($domain)) throw new Exception('函数计算域名不能为空'); + if (empty($fc_cname)) throw new Exception('域名CNAME地址不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new AliyunNewClient($this->AccessKeyId, $this->AccessKeySecret, $fc_cname, '2023-03-30', $this->proxy); + + try { + $data = $client->request('GET', 'GetCustomDomain', '/2023-03-30/custom-domains/' . $domain); + } catch (Exception $e) { + throw new Exception('获取绑定域名信息失败:' . $e->getMessage()); + } + $this->log('获取函数计算绑定域名信息成功'); + + if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) { + $this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作'); + return; + } + + if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS'; + $data['certConfig']['certName'] = $cert_name; + $data['certConfig']['certificate'] = $fullchain; + $data['certConfig']['privateKey'] = $privatekey; + + $param = [ + 'authConfig' => $data['authConfig'], + 'certConfig' => $data['certConfig'], + 'protocol' => $data['protocol'], + 'routeConfig' => $data['routeConfig'], + 'tlsConfig' => $data['tlsConfig'], + 'wafConfig' => $data['wafConfig'], + ]; + $client->request('PUT', 'UpdateCustomDomain', '/2023-03-30/custom-domains/' . $domain, $param); + + $this->log('函数计算域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_fc2($fullchain, $privatekey, $config) + { + $domain = $config['domain']; + $fc_cname = $config['fc_cname']; + if (empty($domain)) throw new Exception('函数计算域名不能为空'); + if (empty($fc_cname)) throw new Exception('域名CNAME地址不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new AliyunNewClient($this->AccessKeyId, $this->AccessKeySecret, $fc_cname, '2021-04-06', $this->proxy); + + try { + $data = $client->request('GET', 'GetCustomDomain', '/2021-04-06/custom-domains/' . $domain); + } catch (Exception $e) { + throw new Exception('获取绑定域名信息失败:' . $e->getMessage()); + } + $this->log('获取函数计算绑定域名信息成功'); + + if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) { + $this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作'); + return; + } + + if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS'; + $data['certConfig']['certName'] = $cert_name; + $data['certConfig']['certificate'] = $fullchain; + $data['certConfig']['privateKey'] = $privatekey; + + $param = [ + 'protocol' => $data['protocol'], + 'routeConfig' => $data['routeConfig'], + 'certConfig' => $data['certConfig'], + 'tlsConfig' => $data['tlsConfig'], + 'wafConfig' => $data['wafConfig'], + ]; + $client->request('PUT', 'UpdateCustomDomain', '/2021-04-06/custom-domains/' . $domain, $param); + + $this->log('函数计算域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_clb($cert_id, $cert_name, $config) + { + if (empty($config['clb_id'])) throw new Exception('负载均衡实例ID不能为空'); + if (empty($config['clb_port'])) throw new Exception('HTTPS监听端口不能为空'); + + $endpoint = 'slb.' . $config['regionid'] . '.aliyuncs.com'; + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2014-05-15', $this->proxy); + + $param = [ + 'Action' => 'DescribeServerCertificates', + 'RegionId' => $config['regionid'], + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('获取服务器证书列表失败:' . $e->getMessage()); + } + + $ServerCertificateId = null; + foreach ($data['ServerCertificates']['ServerCertificate'] as $cert) { + if ($cert['IsAliCloudCertificate'] == 1 && $cert['AliCloudCertificateId'] == $cert_id) { + $ServerCertificateId = $cert['ServerCertificateId']; + break; + } + } + if (!$ServerCertificateId) { + $param = [ + 'Action' => 'UploadServerCertificate', + 'RegionId' => $config['regionid'], + 'AliCloudCertificateId' => $cert_id, + 'AliCloudCertificateName' => $cert_name, + 'AliCloudCertificateRegionId' => 'cn-hangzhou', + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('服务器证书添加失败:' . $e->getMessage()); + } + $ServerCertificateId = $data['ServerCertificateId']; + $this->log('服务器证书添加成功 ServerCertificateId=' . $ServerCertificateId); + } else { + $this->log('找到已添加的服务器证书 ServerCertificateId=' . $ServerCertificateId); + } + + $deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0; + if ($deploy_type == 1) { + if (empty($config['clb_domain'])) throw new Exception('扩展域名不能为空'); + $domains = explode(',', $config['clb_domain']); + $param = [ + 'Action' => 'DescribeDomainExtensions', + 'RegionId' => $config['regionid'], + 'LoadBalancerId' => $config['clb_id'], + 'ListenerPort' => $config['clb_port'], + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('扩展域名列表查询失败:' . $e->getMessage()); + } + foreach ($data['DomainExtensions']['DomainExtension'] as $item) { + if (in_array($item['Domain'], $domains)) { + if ($ServerCertificateId == $item['ServerCertificateId']) { + $this->log('负载均衡HTTPS扩展域名 ' . $item['Domain'] . ' 证书已配置'); + } else { + $param = [ + 'Action' => 'SetDomainExtensionAttribute', + 'RegionId' => $config['regionid'], + 'DomainExtensionId' => $item['DomainExtensionId'], + 'ServerCertificateId' => $ServerCertificateId, + ]; + $client->request($param); + $this->log('负载均衡HTTPS扩展域名 ' . $item['Domain'] . ' 证书更新成功'); + } + } + } + } else { + $param = [ + 'Action' => 'DescribeLoadBalancerHTTPSListenerAttribute', + 'RegionId' => $config['regionid'], + 'LoadBalancerId' => $config['clb_id'], + 'ListenerPort' => $config['clb_port'], + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('HTTPS监听配置查询失败:' . $e->getMessage()); + } + + if ($data['ServerCertificateId'] == $ServerCertificateId) { + $this->log('负载均衡HTTPS监听已配置该证书,无需重复操作'); + return; + } + + $param = [ + 'Action' => 'SetLoadBalancerHTTPSListenerAttribute', + 'RegionId' => $config['regionid'], + 'LoadBalancerId' => $config['clb_id'], + 'ListenerPort' => $config['clb_port'], + 'ServerCertificateId' => $ServerCertificateId, + ]; + $client->request($param); + $this->log('负载均衡HTTPS监听证书配置成功!'); + } + } + + private function deploy_alb($cert_id, $config) + { + if (empty($config['alb_listener_id'])) throw new Exception('负载均衡监听ID不能为空'); + + $endpoint = 'alb.' . $config['regionid'] . '.aliyuncs.com'; + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2020-06-16', $this->proxy); + $cert_id = $cert_id . '-cn-hangzhou'; + $deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0; + + if ($deploy_type == 1) { + $param = [ + 'Action' => 'ListListenerCertificates', + 'MaxResults' => 100, + 'ListenerId' => $config['alb_listener_id'], + 'CertificateType' => 'Server', + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('获取监听证书列表失败:' . $e->getMessage()); + } + foreach ($data['Certificates'] as $cert) { + if ($cert['CertificateId'] == $cert_id) { + $this->log('负载均衡监听扩展证书已添加,无需重复操作'); + return; + } + } + + $param = [ + 'Action' => 'AssociateAdditionalCertificatesWithListener', + 'ListenerId' => $config['alb_listener_id'], + 'Certificates.1.CertificateId' => $cert_id, + ]; + $client->request($param); + $this->log('应用型负载均衡监听扩展证书添加成功!'); + } else { + $param = [ + 'Action' => 'UpdateListenerAttribute', + 'ListenerId' => $config['alb_listener_id'], + 'Certificates.1.CertificateId' => $cert_id, + ]; + $client->request($param); + $this->log('应用型负载均衡监听默认证书更新成功!'); + } + } + + private function deploy_nlb($cert_id, $config) + { + if (empty($config['nlb_listener_id'])) throw new Exception('负载均衡监听ID不能为空'); + + $endpoint = 'nlb.' . $config['regionid'] . '.aliyuncs.com'; + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2022-04-30', $this->proxy); + $cert_id = $cert_id . '-cn-hangzhou'; + $deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0; + + if ($deploy_type == 1) { + $param = [ + 'Action' => 'ListListenerCertificates', + 'MaxResults' => 50, + 'ListenerId' => $config['nlb_listener_id'], + 'CertificateType' => 'Server', + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('获取监听证书列表失败:' . $e->getMessage()); + } + foreach ($data['Certificates'] as $cert) { + if ($cert['CertificateId'] == $cert_id) { + $this->log('负载均衡监听扩展证书已添加,无需重复操作'); + return; + } + } + + $param = [ + 'Action' => 'AssociateAdditionalCertificatesWithListener', + 'ListenerId' => $config['nlb_listener_id'], + 'AdditionalCertificateIds.1' => $cert_id, + ]; + $client->request($param); + $this->log('网络型负载均衡监听扩展证书添加成功!'); + } else { + $param = [ + 'Action' => 'UpdateListenerAttribute', + 'ListenerId' => $config['nlb_listener_id'], + 'CertificateIds.1' => $cert_id, + ]; + $client->request($param); + $this->log('网络型负载均衡监听默认证书更新成功!'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/aws.php b/app/lib/deploy/aws.php index 9470c25..92d9d20 100644 --- a/app/lib/deploy/aws.php +++ b/app/lib/deploy/aws.php @@ -1,150 +1,150 @@ -AccessKeyId = $config['AccessKeyId']; - $this->SecretAccessKey = $config['SecretAccessKey']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - } - - public function check() - { - if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); - $client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'iam.amazonaws.com', 'iam', '2010-05-08', 'us-east-1', $this->proxy); - $client->requestXml('GET', 'GetUser'); - return true; - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if ($config['product'] == 'acm') { - if (empty($config['acm_arn'])) throw new Exception('ACM ARN不能为空'); - $this->get_cert_id($fullchain, $privatekey, $config['acm_arn'], true); - } else { - $this->deploy_cloudfront($fullchain, $privatekey, $config, $info); - } - } - - private function deploy_cloudfront($fullchain, $privatekey, $config, &$info) - { - if (empty($config['distribution_id'])) throw new Exception('分配ID不能为空'); - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - - $cert_id = isset($info['cert_id']) ? $info['cert_id'] : null; - $cert_id = $this->get_cert_id($fullchain, $privatekey, $cert_id); - usleep(500000); - - $client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'cloudfront.amazonaws.com', 'cloudfront', '2020-05-31', 'us-east-1', $this->proxy); - try { - $data = $client->requestXmlN('GET', '/distribution/' . $config['distribution_id'] . '/config', [], null, true); - } catch (Exception $e) { - throw new Exception('获取分配信息失败:' . $e->getMessage()); - } - - $data['ViewerCertificate']['ACMCertificateArn'] = $cert_id; - $data['ViewerCertificate']['CloudFrontDefaultCertificate'] = 'false'; - unset($data['ViewerCertificate']['Certificate']); - unset($data['ViewerCertificate']['CertificateSource']); - - $xml = new \SimpleXMLElement(''); - $client->requestXmlN('PUT', '/distribution/' . $config['distribution_id'] . '/config', $data, $xml); - $this->log('分配ID: ' . $config['distribution_id'] . ' 证书部署成功!'); - } - - private function get_cert_id($fullchain, $privatekey, $cert_id = null, $acm = false) - { - if ($acm === true && $cert_id == null) { - throw new Exception('ACM ARN不能为空'); - } - - $certificates = explode('-----END CERTIFICATE-----', $fullchain); - $cert = $certificates[0] . '-----END CERTIFICATE-----'; - - $client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'acm.us-east-1.amazonaws.com', 'acm', '', 'us-east-1', $this->proxy); - - if (!empty($cert_id)) { - try { - $data = $client->request('POST', 'CertificateManager.GetCertificate', [ - 'CertificateArn' => $cert_id - ]); - // 如果成功获取证书信息,说明证书存在,直接返回cert_id - if (isset($data['Certificate']) && trim($data['Certificate']) == trim($cert)) { - $this->log('证书已是最新,ACM ARN:' . $cert_id); - return $cert_id; - } else { - $this->log('证书已过期或被删除,准备更新或者重新上传'); - } - } catch (Exception $e) { - if ($acm === true) { - throw new Exception('获取证书信息失败,请检查ACM ARN是否正确:' . $e->getMessage()); - } - $this->log('证书已被删除:' . $cert_id. ',准备重新上传'); - } - } - - $certificateChain = ''; - if (count($certificates) > 1) { - // 从第二个证书开始,重新拼接中间证书链 - for ($i = 1; $i < count($certificates); $i++) { - if (trim($certificates[$i]) !== '') { // 忽略空字符串(可能由末尾分割产生) - $certificateChain .= $certificates[$i] . '-----END CERTIFICATE-----'; - } - } - } - - $param = [ - 'Certificate' => base64_encode($cert), - 'PrivateKey' => base64_encode($privatekey), - ]; - - // 如果有中间证书链,则添加到参数中 - if (!empty($certificateChain)) { - $param['CertificateChain'] = base64_encode($certificateChain); - } - - // 如果是ACM,则添加ARN参数,用于更新证书 - if ($acm === true) { - $param['CertificateArn'] = $cert_id; - } - - $client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'acm.us-east-1.amazonaws.com', 'acm', '', 'us-east-1', $this->proxy); - try { - $data = $client->request('POST', 'CertificateManager.ImportCertificate', $param); - $cert_id = $data['CertificateArn']; - } catch (Exception $e) { - throw new Exception('上传证书失败:' . $e->getMessage()); - } - - $this->log('证书上传成功:' . $cert_id); - - $info['cert_id'] = $cert_id; - - return $cert_id; - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'iam.amazonaws.com', 'iam', '2010-05-08', 'us-east-1', $this->proxy); + $client->requestXml('GET', 'GetUser'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if ($config['product'] == 'acm') { + if (empty($config['acm_arn'])) throw new Exception('ACM ARN不能为空'); + $this->get_cert_id($fullchain, $privatekey, $config['acm_arn'], true); + } else { + $this->deploy_cloudfront($fullchain, $privatekey, $config, $info); + } + } + + private function deploy_cloudfront($fullchain, $privatekey, $config, &$info) + { + if (empty($config['distribution_id'])) throw new Exception('分配ID不能为空'); + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + + $cert_id = isset($info['cert_id']) ? $info['cert_id'] : null; + $cert_id = $this->get_cert_id($fullchain, $privatekey, $cert_id); + usleep(500000); + + $client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'cloudfront.amazonaws.com', 'cloudfront', '2020-05-31', 'us-east-1', $this->proxy); + try { + $data = $client->requestXmlN('GET', '/distribution/' . $config['distribution_id'] . '/config', [], null, true); + } catch (Exception $e) { + throw new Exception('获取分配信息失败:' . $e->getMessage()); + } + + $data['ViewerCertificate']['ACMCertificateArn'] = $cert_id; + $data['ViewerCertificate']['CloudFrontDefaultCertificate'] = 'false'; + unset($data['ViewerCertificate']['Certificate']); + unset($data['ViewerCertificate']['CertificateSource']); + + $xml = new \SimpleXMLElement(''); + $client->requestXmlN('PUT', '/distribution/' . $config['distribution_id'] . '/config', $data, $xml); + $this->log('分配ID: ' . $config['distribution_id'] . ' 证书部署成功!'); + } + + private function get_cert_id($fullchain, $privatekey, $cert_id = null, $acm = false) + { + if ($acm === true && $cert_id == null) { + throw new Exception('ACM ARN不能为空'); + } + + $certificates = explode('-----END CERTIFICATE-----', $fullchain); + $cert = $certificates[0] . '-----END CERTIFICATE-----'; + + $client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'acm.us-east-1.amazonaws.com', 'acm', '', 'us-east-1', $this->proxy); + + if (!empty($cert_id)) { + try { + $data = $client->request('POST', 'CertificateManager.GetCertificate', [ + 'CertificateArn' => $cert_id + ]); + // 如果成功获取证书信息,说明证书存在,直接返回cert_id + if (isset($data['Certificate']) && trim($data['Certificate']) == trim($cert)) { + $this->log('证书已是最新,ACM ARN:' . $cert_id); + return $cert_id; + } else { + $this->log('证书已过期或被删除,准备更新或者重新上传'); + } + } catch (Exception $e) { + if ($acm === true) { + throw new Exception('获取证书信息失败,请检查ACM ARN是否正确:' . $e->getMessage()); + } + $this->log('证书已被删除:' . $cert_id. ',准备重新上传'); + } + } + + $certificateChain = ''; + if (count($certificates) > 1) { + // 从第二个证书开始,重新拼接中间证书链 + for ($i = 1; $i < count($certificates); $i++) { + if (trim($certificates[$i]) !== '') { // 忽略空字符串(可能由末尾分割产生) + $certificateChain .= $certificates[$i] . '-----END CERTIFICATE-----'; + } + } + } + + $param = [ + 'Certificate' => base64_encode($cert), + 'PrivateKey' => base64_encode($privatekey), + ]; + + // 如果有中间证书链,则添加到参数中 + if (!empty($certificateChain)) { + $param['CertificateChain'] = base64_encode($certificateChain); + } + + // 如果是ACM,则添加ARN参数,用于更新证书 + if ($acm === true) { + $param['CertificateArn'] = $cert_id; + } + + $client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'acm.us-east-1.amazonaws.com', 'acm', '', 'us-east-1', $this->proxy); + try { + $data = $client->request('POST', 'CertificateManager.ImportCertificate', $param); + $cert_id = $data['CertificateArn']; + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + + $this->log('证书上传成功:' . $cert_id); + + $info['cert_id'] = $cert_id; + + return $cert_id; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/baidu.php b/app/lib/deploy/baidu.php index 7476416..de66efd 100644 --- a/app/lib/deploy/baidu.php +++ b/app/lib/deploy/baidu.php @@ -1,160 +1,160 @@ -AccessKeyId = $config['AccessKeyId']; - $this->SecretAccessKey = $config['SecretAccessKey']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - } - - public function check() - { - if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); - $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'cdn.baidubce.com', $this->proxy); - $client->request('GET', '/v2/domain'); - return true; - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if (!isset($config['product']) || $config['product'] == 'cdn') { - $this->deploy_cdn($fullchain, $privatekey, $config, $info); - } else { - $cert_id = $this->get_cert_id($fullchain, $privatekey); - $info['cert_id'] = $cert_id; - if ($config['product'] == 'blb') { - $this->deploy_blb($cert_id, $config); - } elseif ($config['product'] == 'appblb') { - $this->deploy_appblb($cert_id, $config); - } else { - throw new Exception('不支持的产品类型'); - } - } - } - - public function deploy_cdn($fullchain, $privatekey, $config, &$info) - { - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'cdn.baidubce.com', $this->proxy); - $param = [ - 'httpsEnable' => 'ON', - 'certificate' => [ - 'certName' => $config['cert_name'], - 'certServerData' => $fullchain, - 'certPrivateData' => $privatekey, - ], - ]; - foreach (explode(',', $config['domain']) as $domain) { - if (empty($domain)) continue; - try { - $data = $client->request('GET', '/v2/' . $domain . '/certificates'); - if (isset($data['certName']) && $data['certName'] == $config['cert_name']) { - $this->log('CDN域名 ' . $domain . ' 证书已存在,无需重复部署'); - return; - } - } catch (Exception $e) { - $this->log($e->getMessage()); - } - - $data = $client->request('PUT', '/v2/' . $domain . '/certificates', null, $param); - $info['cert_id'] = $data['certId']; - $this->log('CDN域名 ' . $domain . ' 证书部署成功!'); - } - } - - public function deploy_blb($cert_id, $config) - { - if (empty($config['blb_id'])) throw new Exception('负载均衡实例ID不能为空'); - if (empty($config['blb_port'])) throw new Exception('HTTPS监听端口不能为空'); - $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'blb.' . $config['region'] . '.baidubce.com', $this->proxy); - $query = [ - 'listenerPort' => $config['blb_port'], - ]; - $param = [ - 'certIds' => [$cert_id], - ]; - $client->request('PUT', '/v1/blb/' . $config['blb_id'] . '/HTTPSlistener', $query, $param); - $this->log('普通型BLB ' . $config['blb_id'] . ' 部署证书成功!'); - } - - public function deploy_appblb($cert_id, $config) - { - if (empty($config['blb_id'])) throw new Exception('负载均衡实例ID不能为空'); - if (empty($config['blb_port'])) throw new Exception('HTTPS监听端口不能为空'); - $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'blb.' . $config['region'] . '.baidubce.com', $this->proxy); - $query = [ - 'listenerPort' => $config['blb_port'], - ]; - $param = [ - 'certIds' => [$cert_id], - ]; - $client->request('PUT', '/v1/appblb/' . $config['blb_id'] . '/HTTPSlistener', $query, $param); - $this->log('应用型BLB ' . $config['blb_id'] . ' 部署证书成功!'); - } - - private function get_cert_id($fullchain, $privatekey) - { - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'certificate.baidubce.com', $this->proxy); - $query = [ - 'certName' => $cert_name, - ]; - try { - $data = $client->request('GET', '/v1/certificate', $query); - } catch (Exception $e) { - throw new Exception('查找证书失败:' . $e->getMessage()); - } - foreach ($data['certs'] as $row) { - if ($row['certName'] == $cert_name) { - $this->log('证书已存在 CertId=' . $row['certId']); - return $row['certId']; - } - } - - $param = [ - 'certName' => $cert_name, - 'certServerData' => $fullchain, - 'certPrivateData' => $privatekey, - ]; - try { - $data = $client->request('POST', '/v1/certificate', null, $param); - } catch (Exception $e) { - throw new Exception('上传证书失败:' . $e->getMessage()); - } - $cert_id = $data['certId']; - $this->log('上传证书成功 CertId=' . $cert_id); - return $cert_id; - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'cdn.baidubce.com', $this->proxy); + $client->request('GET', '/v2/domain'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (!isset($config['product']) || $config['product'] == 'cdn') { + $this->deploy_cdn($fullchain, $privatekey, $config, $info); + } else { + $cert_id = $this->get_cert_id($fullchain, $privatekey); + $info['cert_id'] = $cert_id; + if ($config['product'] == 'blb') { + $this->deploy_blb($cert_id, $config); + } elseif ($config['product'] == 'appblb') { + $this->deploy_appblb($cert_id, $config); + } else { + throw new Exception('不支持的产品类型'); + } + } + } + + public function deploy_cdn($fullchain, $privatekey, $config, &$info) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'cdn.baidubce.com', $this->proxy); + $param = [ + 'httpsEnable' => 'ON', + 'certificate' => [ + 'certName' => $config['cert_name'], + 'certServerData' => $fullchain, + 'certPrivateData' => $privatekey, + ], + ]; + foreach (explode(',', $config['domain']) as $domain) { + if (empty($domain)) continue; + try { + $data = $client->request('GET', '/v2/' . $domain . '/certificates'); + if (isset($data['certName']) && $data['certName'] == $config['cert_name']) { + $this->log('CDN域名 ' . $domain . ' 证书已存在,无需重复部署'); + return; + } + } catch (Exception $e) { + $this->log($e->getMessage()); + } + + $data = $client->request('PUT', '/v2/' . $domain . '/certificates', null, $param); + $info['cert_id'] = $data['certId']; + $this->log('CDN域名 ' . $domain . ' 证书部署成功!'); + } + } + + public function deploy_blb($cert_id, $config) + { + if (empty($config['blb_id'])) throw new Exception('负载均衡实例ID不能为空'); + if (empty($config['blb_port'])) throw new Exception('HTTPS监听端口不能为空'); + $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'blb.' . $config['region'] . '.baidubce.com', $this->proxy); + $query = [ + 'listenerPort' => $config['blb_port'], + ]; + $param = [ + 'certIds' => [$cert_id], + ]; + $client->request('PUT', '/v1/blb/' . $config['blb_id'] . '/HTTPSlistener', $query, $param); + $this->log('普通型BLB ' . $config['blb_id'] . ' 部署证书成功!'); + } + + public function deploy_appblb($cert_id, $config) + { + if (empty($config['blb_id'])) throw new Exception('负载均衡实例ID不能为空'); + if (empty($config['blb_port'])) throw new Exception('HTTPS监听端口不能为空'); + $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'blb.' . $config['region'] . '.baidubce.com', $this->proxy); + $query = [ + 'listenerPort' => $config['blb_port'], + ]; + $param = [ + 'certIds' => [$cert_id], + ]; + $client->request('PUT', '/v1/appblb/' . $config['blb_id'] . '/HTTPSlistener', $query, $param); + $this->log('应用型BLB ' . $config['blb_id'] . ' 部署证书成功!'); + } + + private function get_cert_id($fullchain, $privatekey) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'certificate.baidubce.com', $this->proxy); + $query = [ + 'certName' => $cert_name, + ]; + try { + $data = $client->request('GET', '/v1/certificate', $query); + } catch (Exception $e) { + throw new Exception('查找证书失败:' . $e->getMessage()); + } + foreach ($data['certs'] as $row) { + if ($row['certName'] == $cert_name) { + $this->log('证书已存在 CertId=' . $row['certId']); + return $row['certId']; + } + } + + $param = [ + 'certName' => $cert_name, + 'certServerData' => $fullchain, + 'certPrivateData' => $privatekey, + ]; + try { + $data = $client->request('POST', '/v1/certificate', null, $param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $cert_id = $data['certId']; + $this->log('上传证书成功 CertId=' . $cert_id); + return $cert_id; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/baishan.php b/app/lib/deploy/baishan.php index 61915e3..d498345 100644 --- a/app/lib/deploy/baishan.php +++ b/app/lib/deploy/baishan.php @@ -1,85 +1,85 @@ -token = $config['token']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->token)) throw new Exception('token不能为空'); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if (empty($config['id'])) throw new Exception('证书ID不能为空'); - - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $params = [ - 'cert_id' => $config['id'], - 'name' => $cert_name, - 'certificate' => $fullchain, - 'key' => $privatekey, - ]; - try { - $this->request('/v2/domain/certificate?token=' . $this->token, $params); - } catch (Exception $e) { - if (strpos($e->getMessage(), 'this certificate is exists') !== false) { - $this->log('证书ID:' . $config['id'] . '已存在,无需更新'); - return; - } - throw new Exception($e->getMessage()); - } - - $this->log('证书ID:' . $config['id'] . '更新成功!'); - } - - private function request($path, $params = null) - { - $url = $this->url . $path; - $headers = []; - $body = null; - if ($params) { - $headers['Content-Type'] = 'application/json'; - $body = json_encode($params); - } - $response = http_request($url, $body, null, null, $headers, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['code']) && $result['code'] == 0) { - return $result; - } elseif (isset($result['message'])) { - throw new Exception($result['message']); - } else { - if (!empty($response['body'])) $this->log('Response:' . $response['body']); - throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +token = $config['token']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->token)) throw new Exception('token不能为空'); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['id'])) throw new Exception('证书ID不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $params = [ + 'cert_id' => $config['id'], + 'name' => $cert_name, + 'certificate' => $fullchain, + 'key' => $privatekey, + ]; + try { + $this->request('/v2/domain/certificate?token=' . $this->token, $params); + } catch (Exception $e) { + if (strpos($e->getMessage(), 'this certificate is exists') !== false) { + $this->log('证书ID:' . $config['id'] . '已存在,无需更新'); + return; + } + throw new Exception($e->getMessage()); + } + + $this->log('证书ID:' . $config['id'] . '更新成功!'); + } + + private function request($path, $params = null) + { + $url = $this->url . $path; + $headers = []; + $body = null; + if ($params) { + $headers['Content-Type'] = 'application/json'; + $body = json_encode($params); + } + $response = http_request($url, $body, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 0) { + return $result; + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + if (!empty($response['body'])) $this->log('Response:' . $response['body']); + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/btpanel.php b/app/lib/deploy/btpanel.php index 06a5218..5630630 100644 --- a/app/lib/deploy/btpanel.php +++ b/app/lib/deploy/btpanel.php @@ -1,335 +1,335 @@ -url = rtrim($config['url'], '/'); - $this->key = $config['key']; - $this->version = isset($config['version']) ? intval($config['version']) : 0; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥'); - - if ($this->version == 1) { - $path = '/config/get_config'; - $response = $this->request($path, []); - $result = json_decode($response, true); - if (isset($result['panel']['status']) && $result['panel']['status']) { - return true; - } else { - throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接'); - } - } else { - $path = '/config?action=get_config'; - $response = $this->request($path, []); - $result = json_decode($response, true); - if (isset($result['status']) && ($result['status'] == 1 || isset($result['sites_path']))) { - return true; - } else { - throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接'); - } - } - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if ($config['type'] == '1') { - $this->deployPanel($fullchain, $privatekey); - $this->log("面板证书部署成功"); - return; - } - - $isIIS = $config['type'] == '0' && $this->version == 1 && isset($config['is_iis']) && $config['is_iis'] == '1'; - if ($isIIS) { - $response = $this->request('/panel/get_config', []); - $result = json_decode($response, true); - if (isset($result['paths']['soft'])) { - if ($result['config']['webserver'] != 'iis') { - throw new Exception('当前安装的Web服务器不是IIS'); - } - $panel_path = $result['paths']['soft']; - } else { - throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接'); - } - - $pfx_dir = $panel_path . '/temp/ssl/' . getMillisecond(); - $pfx_path = $pfx_dir . '/cert.pfx'; - $pfx_password = '123456'; - $pfx = CertHelper::getPfx($fullchain, $privatekey, $pfx_password); - $data = [ - ['name' => 'path', 'contents' => $pfx_dir], - ['name' => 'filename', 'contents' => 'cert.pfx'], - ['name' => 'size', 'contents' => strlen($pfx)], - ['name' => 'start', 'contents' => '0'], - ['name' => 'blob', 'filename' => 'cert.pfx', 'contents' => $pfx], - ['name' => 'force', 'contents' => 'true'], - ]; - $response = $this->request('/files/upload', $data, true); - $result = json_decode($response, true); - if (isset($result['status']) && $result['status']) { - } else { - throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接'); - } - } - - $sites = explode("\n", $config['sites']); - $success = 0; - $errmsg = null; - foreach ($sites as $site) { - $siteName = trim($site); - if (empty($siteName)) continue; - if ($config['type'] == '3') { - try { - $this->deployDocker($siteName, $fullchain, $privatekey); - $this->log("Docker域名 {$siteName} 证书部署成功"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("Docker域名 {$siteName} 证书部署失败:" . $errmsg); - } - } elseif ($config['type'] == '2') { - try { - $this->deployMailSys($siteName, $fullchain, $privatekey); - $this->log("邮局域名 {$siteName} 证书部署成功"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("邮局域名 {$siteName} 证书部署失败:" . $errmsg); - } - } elseif ($isIIS) { - try { - $this->deployIISSite($siteName, $pfx_path, $pfx_password); - $this->log("域名 {$siteName} 证书部署成功"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("域名 {$siteName} 证书部署失败:" . $errmsg); - } - } else { - try { - $this->deploySite($siteName, $fullchain, $privatekey); - $this->log("网站 {$siteName} 证书部署成功"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("网站 {$siteName} 证书部署失败:" . $errmsg); - } - } - } - if ($success == 0) { - throw new Exception($errmsg ? $errmsg : '要部署的网站不存在'); - } - } - - private function deployPanel($fullchain, $privatekey) - { - if ($this->version == 1) { - $path = '/config/set_panel_ssl'; - $data = [ - 'ssl_key' => $privatekey, - 'ssl_pem' => $fullchain, - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['status']) && $result['status']) { - return true; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } else { - $path = '/config?action=SavePanelSSL'; - $data = [ - 'privateKey' => $privatekey, - 'certPem' => $fullchain, - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['status']) && $result['status']) { - return true; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } - } - - private function deploySite($siteName, $fullchain, $privatekey) - { - if ($this->version == 1) { - $path = '/datalist/get_data_list'; - $data = [ - 'table' => 'sites', - 'search_type' => 'PHP', - 'search' => $siteName, - 'p' => 1, - 'limit' => 10, - 'type' => -1, - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['data'])) { - if (empty($result['data'])) throw new Exception("网站 {$siteName} 不存在"); - $siteId = null; - foreach ($result['data'] as $item) { - if ($item['name'] == $siteName) { - $siteId = $item['id']; - break; - } - } - if (is_null($siteId)) throw new Exception("网站 {$siteName} 不存在"); - $path = '/site/set_site_ssl'; - $data = [ - 'siteid' => $siteId, - 'status' => 'true', - 'sslType' => '', - 'cert' => $fullchain, - 'key' => $privatekey, - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['status']) && $result['status']) { - return true; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - return true; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } else { - $path = '/site?action=SetSSL'; - $data = [ - 'type' => '0', - 'siteName' => $siteName, - 'key' => $privatekey, - 'csr' => $fullchain, - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['status']) && $result['status']) { - return true; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } - } - - private function deployIISSite($domain, $pfx_path, $password = '123456') - { - $path = '/site/set_site_domain_ssl'; - $data = [ - 'domain' => $domain, - 'path' => $pfx_path, - 'password' => $password, - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['status']) && $result['status']) { - return true; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } - - private function deployMailSys($domain, $fullchain, $privatekey) - { - $path = '/plugin?action=a&name=mail_sys&s=set_mail_certificate_multiple'; - $data = [ - 'domain' => $domain, - 'key' => $privatekey, - 'csr' => $fullchain, - 'act' => 'add', - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['status']) && $result['status']) { - return true; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } - - private function deployDocker($domain, $fullchain, $privatekey) - { - $path = '/mod/docker/com/set_ssl'; - $data = [ - 'site_name' => $domain, - 'key' => $privatekey, - 'csr' => $fullchain, - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['status']) && $result['status']) { - return true; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - private function request($path, $params, $file = false) - { - $url = $this->url . $path; - - $now_time = time(); - $headers = []; - if ($file) { - $post_data = [ - ['name' => 'request_token', 'contents' => md5($now_time . md5($this->key))], - ['name' => 'request_time', 'contents' => $now_time], - ]; - $post_data = array_merge($post_data, $params); - $headers['Content-Type'] = 'multipart/form-data'; - } else { - $post_data = [ - 'request_token' => md5($now_time . md5($this->key)), - 'request_time' => $now_time - ]; - $post_data = array_merge($post_data, $params); - } - $response = http_request($url, $post_data, null, null, $headers, $this->proxy); - return $response['body']; - } -} +url = rtrim($config['url'], '/'); + $this->key = $config['key']; + $this->version = isset($config['version']) ? intval($config['version']) : 0; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥'); + + if ($this->version == 1) { + $path = '/config/get_config'; + $response = $this->request($path, []); + $result = json_decode($response, true); + if (isset($result['panel']['status']) && $result['panel']['status']) { + return true; + } else { + throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接'); + } + } else { + $path = '/config?action=get_config'; + $response = $this->request($path, []); + $result = json_decode($response, true); + if (isset($result['status']) && ($result['status'] == 1 || isset($result['sites_path']))) { + return true; + } else { + throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接'); + } + } + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if ($config['type'] == '1') { + $this->deployPanel($fullchain, $privatekey); + $this->log("面板证书部署成功"); + return; + } + + $isIIS = $config['type'] == '0' && $this->version == 1 && isset($config['is_iis']) && $config['is_iis'] == '1'; + if ($isIIS) { + $response = $this->request('/panel/get_config', []); + $result = json_decode($response, true); + if (isset($result['paths']['soft'])) { + if ($result['config']['webserver'] != 'iis') { + throw new Exception('当前安装的Web服务器不是IIS'); + } + $panel_path = $result['paths']['soft']; + } else { + throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接'); + } + + $pfx_dir = $panel_path . '/temp/ssl/' . getMillisecond(); + $pfx_path = $pfx_dir . '/cert.pfx'; + $pfx_password = '123456'; + $pfx = CertHelper::getPfx($fullchain, $privatekey, $pfx_password); + $data = [ + ['name' => 'path', 'contents' => $pfx_dir], + ['name' => 'filename', 'contents' => 'cert.pfx'], + ['name' => 'size', 'contents' => strlen($pfx)], + ['name' => 'start', 'contents' => '0'], + ['name' => 'blob', 'filename' => 'cert.pfx', 'contents' => $pfx], + ['name' => 'force', 'contents' => 'true'], + ]; + $response = $this->request('/files/upload', $data, true); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + } else { + throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接'); + } + } + + $sites = explode("\n", $config['sites']); + $success = 0; + $errmsg = null; + foreach ($sites as $site) { + $siteName = trim($site); + if (empty($siteName)) continue; + if ($config['type'] == '3') { + try { + $this->deployDocker($siteName, $fullchain, $privatekey); + $this->log("Docker域名 {$siteName} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("Docker域名 {$siteName} 证书部署失败:" . $errmsg); + } + } elseif ($config['type'] == '2') { + try { + $this->deployMailSys($siteName, $fullchain, $privatekey); + $this->log("邮局域名 {$siteName} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("邮局域名 {$siteName} 证书部署失败:" . $errmsg); + } + } elseif ($isIIS) { + try { + $this->deployIISSite($siteName, $pfx_path, $pfx_password); + $this->log("域名 {$siteName} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("域名 {$siteName} 证书部署失败:" . $errmsg); + } + } else { + try { + $this->deploySite($siteName, $fullchain, $privatekey); + $this->log("网站 {$siteName} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("网站 {$siteName} 证书部署失败:" . $errmsg); + } + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '要部署的网站不存在'); + } + } + + private function deployPanel($fullchain, $privatekey) + { + if ($this->version == 1) { + $path = '/config/set_panel_ssl'; + $data = [ + 'ssl_key' => $privatekey, + 'ssl_pem' => $fullchain, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } else { + $path = '/config?action=SavePanelSSL'; + $data = [ + 'privateKey' => $privatekey, + 'certPem' => $fullchain, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + } + + private function deploySite($siteName, $fullchain, $privatekey) + { + if ($this->version == 1) { + $path = '/datalist/get_data_list'; + $data = [ + 'table' => 'sites', + 'search_type' => 'PHP', + 'search' => $siteName, + 'p' => 1, + 'limit' => 10, + 'type' => -1, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['data'])) { + if (empty($result['data'])) throw new Exception("网站 {$siteName} 不存在"); + $siteId = null; + foreach ($result['data'] as $item) { + if ($item['name'] == $siteName) { + $siteId = $item['id']; + break; + } + } + if (is_null($siteId)) throw new Exception("网站 {$siteName} 不存在"); + $path = '/site/set_site_ssl'; + $data = [ + 'siteid' => $siteId, + 'status' => 'true', + 'sslType' => '', + 'cert' => $fullchain, + 'key' => $privatekey, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } else { + $path = '/site?action=SetSSL'; + $data = [ + 'type' => '0', + 'siteName' => $siteName, + 'key' => $privatekey, + 'csr' => $fullchain, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + } + + private function deployIISSite($domain, $pfx_path, $password = '123456') + { + $path = '/site/set_site_domain_ssl'; + $data = [ + 'domain' => $domain, + 'path' => $pfx_path, + 'password' => $password, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + + private function deployMailSys($domain, $fullchain, $privatekey) + { + $path = '/plugin?action=a&name=mail_sys&s=set_mail_certificate_multiple'; + $data = [ + 'domain' => $domain, + 'key' => $privatekey, + 'csr' => $fullchain, + 'act' => 'add', + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + + private function deployDocker($domain, $fullchain, $privatekey) + { + $path = '/mod/docker/com/set_ssl'; + $data = [ + 'site_name' => $domain, + 'key' => $privatekey, + 'csr' => $fullchain, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($path, $params, $file = false) + { + $url = $this->url . $path; + + $now_time = time(); + $headers = []; + if ($file) { + $post_data = [ + ['name' => 'request_token', 'contents' => md5($now_time . md5($this->key))], + ['name' => 'request_time', 'contents' => $now_time], + ]; + $post_data = array_merge($post_data, $params); + $headers['Content-Type'] = 'multipart/form-data'; + } else { + $post_data = [ + 'request_token' => md5($now_time . md5($this->key)), + 'request_time' => $now_time + ]; + $post_data = array_merge($post_data, $params); + } + $response = http_request($url, $post_data, null, null, $headers, $this->proxy); + return $response['body']; + } +} diff --git a/app/lib/deploy/btwaf.php b/app/lib/deploy/btwaf.php index b293b96..d5d0646 100644 --- a/app/lib/deploy/btwaf.php +++ b/app/lib/deploy/btwaf.php @@ -1,158 +1,158 @@ -url = rtrim($config['url'], '/'); - $this->key = $config['key']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥'); - - $path = '/api/user/latest_version'; - $response = $this->request($path, []); - $result = json_decode($response, true); - if (isset($result['code']) && $result['code'] == 0) { - return true; - } else { - throw new Exception(isset($result['res']) ? $result['res'] : '面板地址无法连接'); - } - } - - 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) { - $siteName = trim($site); - if (empty($siteName)) continue; - try { - $this->deploySite($siteName, $fullchain, $privatekey); - $this->log("网站 {$siteName} 证书部署成功"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("网站 {$siteName} 证书部署失败:" . $errmsg); - } - } - if ($success == 0) { - throw new Exception($errmsg ? $errmsg : '要部署的网站不存在'); - } - } - - private function deploySite($siteName, $fullchain, $privatekey) - { - $site_id = null; - $listen_ssl_port = ['443']; - $path = '/api/wafmastersite/get_site_list'; - $data = ['p' => 1, 'p_size' => 10, 'site_name' => $siteName]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['code']) && $result['code'] == 0) { - foreach ($result['res']['list'] as $site) { - if ($site['site_name'] == $siteName) { - $site_id = $site['site_id']; - if (isset($site['server']['listen_ssl_port']) && !empty($site['server']['listen_ssl_port'])) { - $listen_ssl_port = $site['server']['listen_ssl_port']; - } - break; - } - } - if (!$site_id) { - throw new Exception("网站名称不存在"); - } - } elseif (isset($result['res'])) { - throw new Exception($result['res']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - $path = '/api/wafmastersite/modify_site'; - $data = [ - 'types' => 'openCert', - 'site_id' => $site_id, - 'server' => [ - 'listen_ssl_port' => $listen_ssl_port, - 'ssl' => [ - 'is_ssl' => 1, - 'private_key' => $privatekey, - 'full_chain' => $fullchain, - ], - ] - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['code']) && $result['code'] == 0) { - return true; - } elseif (isset($result['res'])) { - throw new Exception($result['res']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } - - private function deployPanel($fullchain, $privatekey) - { - $path = '/api/config/set_cert'; - $data = [ - 'certContent' => $fullchain, - 'keyContent' => $privatekey, - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['code']) && $result['code'] == 0) { - return true; - } elseif (isset($result['res'])) { - throw new Exception($result['res']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - private function request($path, $params) - { - $url = $this->url . $path; - - $now_time = time(); - $headers = [ - 'waf_request_time' => $now_time, - 'waf_request_token' => md5($now_time . md5($this->key)), - 'Content-Type' => 'application/json', - ]; - $post = $params ? json_encode($params) : null; - $response = http_request($url, $post, null, null, $headers, $this->proxy, 'POST'); - return $response['body']; - } -} +url = rtrim($config['url'], '/'); + $this->key = $config['key']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥'); + + $path = '/api/user/latest_version'; + $response = $this->request($path, []); + $result = json_decode($response, true); + if (isset($result['code']) && $result['code'] == 0) { + return true; + } else { + throw new Exception(isset($result['res']) ? $result['res'] : '面板地址无法连接'); + } + } + + 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) { + $siteName = trim($site); + if (empty($siteName)) continue; + try { + $this->deploySite($siteName, $fullchain, $privatekey); + $this->log("网站 {$siteName} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("网站 {$siteName} 证书部署失败:" . $errmsg); + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '要部署的网站不存在'); + } + } + + private function deploySite($siteName, $fullchain, $privatekey) + { + $site_id = null; + $listen_ssl_port = ['443']; + $path = '/api/wafmastersite/get_site_list'; + $data = ['p' => 1, 'p_size' => 10, 'site_name' => $siteName]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['code']) && $result['code'] == 0) { + foreach ($result['res']['list'] as $site) { + if ($site['site_name'] == $siteName) { + $site_id = $site['site_id']; + if (isset($site['server']['listen_ssl_port']) && !empty($site['server']['listen_ssl_port'])) { + $listen_ssl_port = $site['server']['listen_ssl_port']; + } + break; + } + } + if (!$site_id) { + throw new Exception("网站名称不存在"); + } + } elseif (isset($result['res'])) { + throw new Exception($result['res']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + $path = '/api/wafmastersite/modify_site'; + $data = [ + 'types' => 'openCert', + 'site_id' => $site_id, + 'server' => [ + 'listen_ssl_port' => $listen_ssl_port, + 'ssl' => [ + 'is_ssl' => 1, + 'private_key' => $privatekey, + 'full_chain' => $fullchain, + ], + ] + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['code']) && $result['code'] == 0) { + return true; + } elseif (isset($result['res'])) { + throw new Exception($result['res']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + + private function deployPanel($fullchain, $privatekey) + { + $path = '/api/config/set_cert'; + $data = [ + 'certContent' => $fullchain, + 'keyContent' => $privatekey, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['code']) && $result['code'] == 0) { + return true; + } elseif (isset($result['res'])) { + throw new Exception($result['res']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($path, $params) + { + $url = $this->url . $path; + + $now_time = time(); + $headers = [ + 'waf_request_time' => $now_time, + 'waf_request_token' => md5($now_time . md5($this->key)), + 'Content-Type' => 'application/json', + ]; + $post = $params ? json_encode($params) : null; + $response = http_request($url, $post, null, null, $headers, $this->proxy, 'POST'); + return $response['body']; + } +} diff --git a/app/lib/deploy/cachefly.php b/app/lib/deploy/cachefly.php index 0c03996..b865f7d 100644 --- a/app/lib/deploy/cachefly.php +++ b/app/lib/deploy/cachefly.php @@ -1,67 +1,67 @@ -apikey = $config['apikey']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->apikey)) throw new Exception('API令牌不能为空'); - $this->request('/accounts/me'); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $params = [ - 'certificate' => $fullchain, - 'certificateKey' => $privatekey, - ]; - $this->request('/certificates', $params); - $this->log('证书上传成功!'); - } - - private function request($path, $params = null, $method = null) - { - $url = $this->url . $path; - $headers = ['x-cf-authorization' => 'Bearer ' . $this->apikey]; - $body = null; - if ($params) { - $headers['Content-Type'] = 'application/json'; - $body = json_encode($params); - } - $response = http_request($url, $body, null, null, $headers, $this->proxy, $method); - $result = json_decode($response['body'], true); - if ($response['code'] >= 200 && $response['code'] < 300) { - return $result; - } else { - if (!empty($response['body'])) $this->log('Response:' . $response['body']); - throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +apikey = $config['apikey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->apikey)) throw new Exception('API令牌不能为空'); + $this->request('/accounts/me'); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $params = [ + 'certificate' => $fullchain, + 'certificateKey' => $privatekey, + ]; + $this->request('/certificates', $params); + $this->log('证书上传成功!'); + } + + private function request($path, $params = null, $method = null) + { + $url = $this->url . $path; + $headers = ['x-cf-authorization' => 'Bearer ' . $this->apikey]; + $body = null; + if ($params) { + $headers['Content-Type'] = 'application/json'; + $body = json_encode($params); + } + $response = http_request($url, $body, null, null, $headers, $this->proxy, $method); + $result = json_decode($response['body'], true); + if ($response['code'] >= 200 && $response['code'] < 300) { + return $result; + } else { + if (!empty($response['body'])) $this->log('Response:' . $response['body']); + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/cdnfly.php b/app/lib/deploy/cdnfly.php index 06b43ed..0904607 100644 --- a/app/lib/deploy/cdnfly.php +++ b/app/lib/deploy/cdnfly.php @@ -1,124 +1,124 @@ -url = rtrim($config['url'], '/'); - $this->api_key = $config['api_key']; - $this->api_secret = $config['api_secret']; - $this->auth = isset($config['auth']) ? $config['auth'] : 0; - if ($this->auth == 1) { - $this->username = $config['username']; - $this->password = $config['password']; - } - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if ($this->auth == 1) { - if (empty($this->url) || empty($this->username) || empty($this->password)) throw new Exception('必填参数不能为空'); - $this->login(); - } else { - if (empty($this->url) || empty($this->api_key) || empty($this->api_secret)) throw new Exception('必填参数不能为空'); - $this->request('/v1/user'); - } - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $id = $config['id']; - if (empty($id)) throw new Exception('证书ID不能为空'); - - $params = [ - 'type' => 'custom', - 'cert' => $fullchain, - 'key' => $privatekey, - ]; - if ($this->auth == 1) { - $access_token = $this->login(); - $url = $this->url . '/v1/certs/' . $id; - $body = json_encode($params); - $headers = [ - 'Access-Token' => $access_token, - ]; - $response = http_request($url, $body, null, null, $headers, $this->proxy, 'PUT'); - $result = json_decode($response['body'], true); - if (isset($result['code']) && $result['code'] == 0) { - } elseif (isset($result['msg'])) { - throw new Exception('证书ID:' . $id . '更新失败,' . $result['msg']); - } else { - throw new Exception('证书ID:' . $id . '更新失败,返回数据解析失败'); - } - } else { - $this->request('/v1/certs/' . $id, $params, 'PUT'); - } - $this->log("证书ID:{$id}更新成功!"); - } - - public function login() - { - $url = $this->url . '/v1/login'; - $params = [ - 'account' => $this->username, - 'password' => $this->password, - ]; - $body = json_encode($params); - $response = http_request($url, $body, null, null, null, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['code']) && $result['code'] == 0) { - return $result['data']['access_token']; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception('登录失败,返回数据解析失败'); - } - } - - private function request($path, $params = null, $method = null) - { - $url = $this->url . $path; - $headers = ['api-key' => $this->api_key, 'api-secret' => $this->api_secret]; - $body = null; - if ($params) { - $headers['Content-Type'] = 'application/json'; - $body = json_encode($params); - } - $response = http_request($url, $body, null, null, $headers, $this->proxy, $method); - $result = json_decode($response['body'], true); - if (isset($result['code']) && $result['code'] == 0) { - return isset($result['data']) ? $result['data'] : null; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception('返回数据解析失败'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +url = rtrim($config['url'], '/'); + $this->api_key = $config['api_key']; + $this->api_secret = $config['api_secret']; + $this->auth = isset($config['auth']) ? $config['auth'] : 0; + if ($this->auth == 1) { + $this->username = $config['username']; + $this->password = $config['password']; + } + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if ($this->auth == 1) { + if (empty($this->url) || empty($this->username) || empty($this->password)) throw new Exception('必填参数不能为空'); + $this->login(); + } else { + if (empty($this->url) || empty($this->api_key) || empty($this->api_secret)) throw new Exception('必填参数不能为空'); + $this->request('/v1/user'); + } + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $id = $config['id']; + if (empty($id)) throw new Exception('证书ID不能为空'); + + $params = [ + 'type' => 'custom', + 'cert' => $fullchain, + 'key' => $privatekey, + ]; + if ($this->auth == 1) { + $access_token = $this->login(); + $url = $this->url . '/v1/certs/' . $id; + $body = json_encode($params); + $headers = [ + 'Access-Token' => $access_token, + ]; + $response = http_request($url, $body, null, null, $headers, $this->proxy, 'PUT'); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 0) { + } elseif (isset($result['msg'])) { + throw new Exception('证书ID:' . $id . '更新失败,' . $result['msg']); + } else { + throw new Exception('证书ID:' . $id . '更新失败,返回数据解析失败'); + } + } else { + $this->request('/v1/certs/' . $id, $params, 'PUT'); + } + $this->log("证书ID:{$id}更新成功!"); + } + + public function login() + { + $url = $this->url . '/v1/login'; + $params = [ + 'account' => $this->username, + 'password' => $this->password, + ]; + $body = json_encode($params); + $response = http_request($url, $body, null, null, null, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 0) { + return $result['data']['access_token']; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception('登录失败,返回数据解析失败'); + } + } + + private function request($path, $params = null, $method = null) + { + $url = $this->url . $path; + $headers = ['api-key' => $this->api_key, 'api-secret' => $this->api_secret]; + $body = null; + if ($params) { + $headers['Content-Type'] = 'application/json'; + $body = json_encode($params); + } + $response = http_request($url, $body, null, null, $headers, $this->proxy, $method); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 0) { + return isset($result['data']) ? $result['data'] : null; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception('返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/ctyun.php b/app/lib/deploy/ctyun.php index 052afd8..01552fa 100644 --- a/app/lib/deploy/ctyun.php +++ b/app/lib/deploy/ctyun.php @@ -1,184 +1,184 @@ -AccessKeyId = $config['AccessKeyId']; - $this->SecretAccessKey = $config['SecretAccessKey']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - } - - public function check() - { - if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); - $client = new CtyunClient($this->AccessKeyId, $this->SecretAccessKey, 'ctcdn-global.ctapi.ctyun.cn', $this->proxy); - $client->request('GET', '/v1/cert/query-cert-list'); - return true; - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - if ($config['product'] == 'cdn') { - $this->deploy_cdn($fullchain, $privatekey, $config); - } elseif ($config['product'] == 'icdn') { - $this->deploy_icdn($fullchain, $privatekey, $config); - } elseif ($config['product'] == 'accessone') { - $this->deploy_accessone($fullchain, $privatekey, $config); - } - } - - private function deploy_cdn($fullchain, $privatekey, $config) - { - $client = new CtyunClient($this->AccessKeyId, $this->SecretAccessKey, 'ctcdn-global.ctapi.ctyun.cn', $this->proxy); - $param = [ - 'name' => $config['cert_name'], - 'key' => $privatekey, - 'certs' => $fullchain, - ]; - try { - $client->request('POST', '/v1/cert/creat-cert', null, $param); - } catch (Exception $e) { - if (strpos($e->getMessage(), '已存在重名的证书') !== false) { - $this->log('已存在重名的证书 cert_name=' . $config['cert_name']); - } else { - throw new Exception('上传证书失败:' . $e->getMessage()); - } - } - $this->log('上传证书成功 cert_name=' . $config['cert_name']); - - $param = [ - 'domain' => $config['domain'], - 'https_status' => 'on', - 'cert_name' => $config['cert_name'], - ]; - try { - $client->request('POST', '/v1/domain/update-domain', null, $param); - } catch (Exception $e) { - if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) { - throw new Exception($e->getMessage()); - } - } - - $this->log('CDN域名 ' . $config['domain'] . ' 部署证书成功!'); - } - - private function deploy_icdn($fullchain, $privatekey, $config) - { - $client = new CtyunClient($this->AccessKeyId, $this->SecretAccessKey, 'icdn-global.ctapi.ctyun.cn', $this->proxy); - $param = [ - 'name' => $config['cert_name'], - 'key' => $privatekey, - 'certs' => $fullchain, - ]; - try { - $client->request('POST', '/v1/cert/creat-cert', null, $param); - } catch (Exception $e) { - if (strpos($e->getMessage(), '已存在重名的证书') !== false) { - $this->log('已存在重名的证书 cert_name=' . $config['cert_name']); - } else { - throw new Exception('上传证书失败:' . $e->getMessage()); - } - } - $this->log('上传证书成功 cert_name=' . $config['cert_name']); - - $param = [ - 'domain' => $config['domain'], - 'https_status' => 'on', - 'cert_name' => $config['cert_name'], - ]; - try { - $client->request('POST', '/v1/domain/update-domain', null, $param); - } catch (Exception $e) { - if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) { - throw new Exception($e->getMessage()); - } - } - - $this->log('CDN域名 ' . $config['domain'] . ' 部署证书成功!'); - } - - private function deploy_accessone($fullchain, $privatekey, $config) - { - $client = new CtyunClient($this->AccessKeyId, $this->SecretAccessKey, 'accessone-global.ctapi.ctyun.cn', $this->proxy); - $param = [ - 'name' => $config['cert_name'], - 'key' => $privatekey, - 'certs' => $fullchain, - ]; - try { - $client->request('POST', '/ctapi/v1/accessone/cert/create', null, $param); - } catch (Exception $e) { - if (strpos($e->getMessage(), '已存在重名的证书') !== false) { - $this->log('已存在重名的证书 cert_name=' . $config['cert_name']); - } else { - throw new Exception('上传证书失败:' . $e->getMessage()); - } - } - $this->log('上传证书成功 cert_name=' . $config['cert_name']); - - $param = [ - 'domain' => $config['domain'], - 'product_code' => '020', - ]; - try { - $result = $client->request('POST', '/ctapi/v1/accessone/domain/config', null, $param); - } catch (Exception $e) { - throw new Exception('查询域名配置失败:' . $e->getMessage()); - } - - if ($result['https_status'] == 'on' && $result['cert_name'] == $config['cert_name']) { - $this->log('边缘安全加速域名 ' . $config['domain'] . ' 证书已部署,无需重复操作!'); - return; - } - - $result['https_status'] = 'on'; - $result['cert_name'] = $config['cert_name']; - $exclude_keys = ['status', 'area_scope', 'cname', 'insert_date', 'status_date', 'record_status', 'record_num', 'customer_name', 'outlink_replace_filter', 'website_ipv6_access_mark', 'websocket_speed', 'dynamic_config', 'dynamic_ability']; - foreach ($result as $key => $value) { - if (in_array($key, $exclude_keys) || is_array($value) && empty($value)) { - unset($result[$key]); - } - } - if (isset($result['origin'])) { - foreach ($result['origin'] as &$origin) { - $origin['weight'] = strval($origin['weight']); - } - } - try { - $client->request('POST', '/ctapi/v1/accessone/domain/modify_config', null, $result); - } catch (Exception $e) { - if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) { - throw new Exception($e->getMessage()); - } - } - - $this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!'); - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $client = new CtyunClient($this->AccessKeyId, $this->SecretAccessKey, 'ctcdn-global.ctapi.ctyun.cn', $this->proxy); + $client->request('GET', '/v1/cert/query-cert-list'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + if ($config['product'] == 'cdn') { + $this->deploy_cdn($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'icdn') { + $this->deploy_icdn($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'accessone') { + $this->deploy_accessone($fullchain, $privatekey, $config); + } + } + + private function deploy_cdn($fullchain, $privatekey, $config) + { + $client = new CtyunClient($this->AccessKeyId, $this->SecretAccessKey, 'ctcdn-global.ctapi.ctyun.cn', $this->proxy); + $param = [ + 'name' => $config['cert_name'], + 'key' => $privatekey, + 'certs' => $fullchain, + ]; + try { + $client->request('POST', '/v1/cert/creat-cert', null, $param); + } catch (Exception $e) { + if (strpos($e->getMessage(), '已存在重名的证书') !== false) { + $this->log('已存在重名的证书 cert_name=' . $config['cert_name']); + } else { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + } + $this->log('上传证书成功 cert_name=' . $config['cert_name']); + + $param = [ + 'domain' => $config['domain'], + 'https_status' => 'on', + 'cert_name' => $config['cert_name'], + ]; + try { + $client->request('POST', '/v1/domain/update-domain', null, $param); + } catch (Exception $e) { + if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) { + throw new Exception($e->getMessage()); + } + } + + $this->log('CDN域名 ' . $config['domain'] . ' 部署证书成功!'); + } + + private function deploy_icdn($fullchain, $privatekey, $config) + { + $client = new CtyunClient($this->AccessKeyId, $this->SecretAccessKey, 'icdn-global.ctapi.ctyun.cn', $this->proxy); + $param = [ + 'name' => $config['cert_name'], + 'key' => $privatekey, + 'certs' => $fullchain, + ]; + try { + $client->request('POST', '/v1/cert/creat-cert', null, $param); + } catch (Exception $e) { + if (strpos($e->getMessage(), '已存在重名的证书') !== false) { + $this->log('已存在重名的证书 cert_name=' . $config['cert_name']); + } else { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + } + $this->log('上传证书成功 cert_name=' . $config['cert_name']); + + $param = [ + 'domain' => $config['domain'], + 'https_status' => 'on', + 'cert_name' => $config['cert_name'], + ]; + try { + $client->request('POST', '/v1/domain/update-domain', null, $param); + } catch (Exception $e) { + if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) { + throw new Exception($e->getMessage()); + } + } + + $this->log('CDN域名 ' . $config['domain'] . ' 部署证书成功!'); + } + + private function deploy_accessone($fullchain, $privatekey, $config) + { + $client = new CtyunClient($this->AccessKeyId, $this->SecretAccessKey, 'accessone-global.ctapi.ctyun.cn', $this->proxy); + $param = [ + 'name' => $config['cert_name'], + 'key' => $privatekey, + 'certs' => $fullchain, + ]; + try { + $client->request('POST', '/ctapi/v1/accessone/cert/create', null, $param); + } catch (Exception $e) { + if (strpos($e->getMessage(), '已存在重名的证书') !== false) { + $this->log('已存在重名的证书 cert_name=' . $config['cert_name']); + } else { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + } + $this->log('上传证书成功 cert_name=' . $config['cert_name']); + + $param = [ + 'domain' => $config['domain'], + 'product_code' => '020', + ]; + try { + $result = $client->request('POST', '/ctapi/v1/accessone/domain/config', null, $param); + } catch (Exception $e) { + throw new Exception('查询域名配置失败:' . $e->getMessage()); + } + + if ($result['https_status'] == 'on' && $result['cert_name'] == $config['cert_name']) { + $this->log('边缘安全加速域名 ' . $config['domain'] . ' 证书已部署,无需重复操作!'); + return; + } + + $result['https_status'] = 'on'; + $result['cert_name'] = $config['cert_name']; + $exclude_keys = ['status', 'area_scope', 'cname', 'insert_date', 'status_date', 'record_status', 'record_num', 'customer_name', 'outlink_replace_filter', 'website_ipv6_access_mark', 'websocket_speed', 'dynamic_config', 'dynamic_ability']; + foreach ($result as $key => $value) { + if (in_array($key, $exclude_keys) || is_array($value) && empty($value)) { + unset($result[$key]); + } + } + if (isset($result['origin'])) { + foreach ($result['origin'] as &$origin) { + $origin['weight'] = strval($origin['weight']); + } + } + try { + $client->request('POST', '/ctapi/v1/accessone/domain/modify_config', null, $result); + } catch (Exception $e) { + if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) { + throw new Exception($e->getMessage()); + } + } + + $this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!'); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/doge.php b/app/lib/deploy/doge.php index 0646519..83809ef 100644 --- a/app/lib/deploy/doge.php +++ b/app/lib/deploy/doge.php @@ -1,124 +1,124 @@ -AccessKey = $config['AccessKey']; - $this->SecretKey = $config['SecretKey']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - } - - public function check() - { - if (empty($this->AccessKey) || empty($this->SecretKey)) throw new Exception('必填参数不能为空'); - $this->request('/cdn/cert/list.json'); - return true; - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $domains = $config['domain']; - if (empty($domains)) throw new Exception('绑定的域名不能为空'); - - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $cert_id = $this->get_cert_id($fullchain, $privatekey, $cert_name); - - foreach (explode(',', $domains) as $domain) { - if (empty($domain)) continue; - $param = [ - 'id' => $cert_id, - 'domain' => $domain, - ]; - $this->request('/cdn/cert/bind.json', $param); - $this->log('CDN域名 ' . $domain . ' 绑定证书成功!'); - } - $info['cert_id'] = $cert_id; - } - - private function get_cert_id($fullchain, $privatekey, $cert_name) - { - $cert_id = null; - - $data = $this->request('/cdn/cert/list.json'); - foreach ($data['certs'] as $cert) { - if ($cert_name == $cert['note']) { - $cert_id = $cert['id']; - $this->log('证书' . $cert_name . '已存在,证书ID:' . $cert_id); - } elseif ($cert['expire'] < time() && $cert['domainCount'] == 0) { - try { - $this->request('/cdn/cert/delete.json', ['id' => $cert['id']]); - $this->log('证书' . $cert['name'] . '已过期,删除证书成功'); - } catch (Exception $e) { - $this->log('证书' . $cert['name'] . '已过期,删除证书失败:' . $e->getMessage()); - } - usleep(300000); - } - } - - if (!$cert_id) { - $param = [ - 'note' => $cert_name, - 'cert' => $fullchain, - 'private' => $privatekey, - ]; - try { - $data = $this->request('/cdn/cert/upload.json', $param); - } catch (Exception $e) { - throw new Exception('上传证书失败:' . $e->getMessage()); - } - $this->log('上传证书成功,证书ID:' . $data['id']); - $cert_id = $data['id']; - usleep(500000); - } - return $cert_id; - } - - private function request($path, $data = null, $json = false) - { - $body = null; - if($data){ - $body = $json ? json_encode($data) : http_build_query($data); - } - $signStr = $path . "\n" . $body; - $sign = hash_hmac('sha1', $signStr, $this->SecretKey); - $authorization = "TOKEN " . $this->AccessKey . ":" . $sign; - $headers = ['Authorization' => $authorization]; - if($body && $json) $headers['Content-Type'] = 'application/json'; - $url = 'https://api.dogecloud.com'.$path; - $response = http_request($url, $body, null, null, $headers, $this->proxy); - $result = json_decode($response['body'], true); - if(isset($result['code']) && $result['code'] == 200){ - return $result['data'] ?? true; - }elseif(isset($result['msg'])){ - throw new Exception($result['msg']); - }else{ - throw new Exception('请求失败'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +AccessKey = $config['AccessKey']; + $this->SecretKey = $config['SecretKey']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + } + + public function check() + { + if (empty($this->AccessKey) || empty($this->SecretKey)) throw new Exception('必填参数不能为空'); + $this->request('/cdn/cert/list.json'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domains = $config['domain']; + if (empty($domains)) throw new Exception('绑定的域名不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $cert_id = $this->get_cert_id($fullchain, $privatekey, $cert_name); + + foreach (explode(',', $domains) as $domain) { + if (empty($domain)) continue; + $param = [ + 'id' => $cert_id, + 'domain' => $domain, + ]; + $this->request('/cdn/cert/bind.json', $param); + $this->log('CDN域名 ' . $domain . ' 绑定证书成功!'); + } + $info['cert_id'] = $cert_id; + } + + private function get_cert_id($fullchain, $privatekey, $cert_name) + { + $cert_id = null; + + $data = $this->request('/cdn/cert/list.json'); + foreach ($data['certs'] as $cert) { + if ($cert_name == $cert['note']) { + $cert_id = $cert['id']; + $this->log('证书' . $cert_name . '已存在,证书ID:' . $cert_id); + } elseif ($cert['expire'] < time() && $cert['domainCount'] == 0) { + try { + $this->request('/cdn/cert/delete.json', ['id' => $cert['id']]); + $this->log('证书' . $cert['name'] . '已过期,删除证书成功'); + } catch (Exception $e) { + $this->log('证书' . $cert['name'] . '已过期,删除证书失败:' . $e->getMessage()); + } + usleep(300000); + } + } + + if (!$cert_id) { + $param = [ + 'note' => $cert_name, + 'cert' => $fullchain, + 'private' => $privatekey, + ]; + try { + $data = $this->request('/cdn/cert/upload.json', $param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $this->log('上传证书成功,证书ID:' . $data['id']); + $cert_id = $data['id']; + usleep(500000); + } + return $cert_id; + } + + private function request($path, $data = null, $json = false) + { + $body = null; + if($data){ + $body = $json ? json_encode($data) : http_build_query($data); + } + $signStr = $path . "\n" . $body; + $sign = hash_hmac('sha1', $signStr, $this->SecretKey); + $authorization = "TOKEN " . $this->AccessKey . ":" . $sign; + $headers = ['Authorization' => $authorization]; + if($body && $json) $headers['Content-Type'] = 'application/json'; + $url = 'https://api.dogecloud.com'.$path; + $response = http_request($url, $body, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if(isset($result['code']) && $result['code'] == 200){ + return $result['data'] ?? true; + }elseif(isset($result['msg'])){ + throw new Exception($result['msg']); + }else{ + throw new Exception('请求失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/fnos.php b/app/lib/deploy/fnos.php index 0d2eac2..221e0ed 100644 --- a/app/lib/deploy/fnos.php +++ b/app/lib/deploy/fnos.php @@ -1,132 +1,132 @@ -config = $config; - } - - public function check() - { - $this->connect(); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $domains = $config['domainList']; - if (empty($domains)) throw new Exception('没有设置要部署的域名'); - - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - - $connection = $this->connect(); - $cert_all = $this->exec($connection, '获取证书列表', 'cat /usr/trim/etc/network_cert_all.conf'); - $list = json_decode($cert_all, true); - if (!$list) throw new Exception('获取证书列表失败'); - - $success = 0; - foreach ($list as $row) { - if (empty($row['san'])) continue; - $cert_domains = $row['san']; - $flag = false; - foreach ($cert_domains as $domain) { - if (in_array($domain, $domains)) { - $flag = true; - break; - } - } - if ($flag) { - $certPath = $row['certificate']; - $keyPath = $row['privateKey']; - $certDir = dirname($certPath); - $this->exec($connection, '上传证书文件', "sudo tee ".$certPath." > /dev/null <<'EOF'\n".$fullchain."\nEOF"); - $this->exec($connection, '上传私钥文件', "sudo tee ".$keyPath." > /dev/null <<'EOF'\n".$privatekey."\nEOF"); - $this->exec($connection, '刷新目录权限', 'sudo chmod 0755 "'.$certDir.'" -R'); - $this->exec($connection, '更新数据表', 'sudo -u postgres psql -d trim_connect -c "UPDATE cert SET valid_to='.$certInfo['validTo_time_t'].'000,valid_from='.$certInfo['validFrom_time_t'].'000,issued_by=\''.$certInfo['issuer']['CN'].'\',updated_time='.getMillisecond().' WHERE private_key=\''.$keyPath.'\'"'); - $this->log('证书 '.$row['domain'].' 更新成功'); - $success++; - } - } - if ($success == 0) { - throw new Exception('没有要更新的证书'); - } else { - $this->exec($connection, '重启webdav', 'sudo systemctl restart webdav.service'); - $this->exec($connection, '重启smbftpd', 'sudo systemctl restart smbftpd.service'); - $this->exec($connection, '重启trim_nginx', 'sudo systemctl restart trim_nginx.service'); - } - } - - private function exec($connection, $name, $cmd) - { - $stream = ssh2_exec($connection, $cmd); - $errorStream = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR); - if (!$stream || !$errorStream) { - throw new Exception($name.'执行命令失败'); - } - stream_set_blocking($stream, true); - stream_set_blocking($errorStream, true); - $output = stream_get_contents($stream); - $errorOutput = stream_get_contents($errorStream); - fclose($stream); - fclose($errorStream); - if (trim($errorOutput)) { - if (strpos($errorOutput, 'a password is required') !== false) { - throw new Exception('权限不足,请先配置 sudo 免密'); - } - throw new Exception($name.'失败:' . trim($errorOutput)); - } else { - if (strlen($output) > 200) { - return $output; - } - $this->log($name.'成功 ' . trim($output)); - return $output; - } - } - - private function connect() - { - if (!function_exists('ssh2_connect')) { - throw new Exception('ssh2扩展未安装'); - } - if (empty($this->config['host']) || empty($this->config['port']) || empty($this->config['username']) || empty($this->config['password'])) { - throw new Exception('必填参数不能为空'); - } - if (!filter_var($this->config['host'], FILTER_VALIDATE_IP) && !filter_var($this->config['host'], FILTER_VALIDATE_DOMAIN)) { - throw new Exception('主机地址不合法'); - } - if (!is_numeric($this->config['port']) || $this->config['port'] < 1 || $this->config['port'] > 65535) { - throw new Exception('SSH端口不合法'); - } - - $connection = ssh2_connect($this->config['host'], intval($this->config['port'])); - if (!$connection) { - throw new Exception('SSH连接失败'); - } - if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) { - throw new Exception('用户名或密码错误'); - } - return $connection; - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +config = $config; + } + + public function check() + { + $this->connect(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domains = $config['domainList']; + if (empty($domains)) throw new Exception('没有设置要部署的域名'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + + $connection = $this->connect(); + $cert_all = $this->exec($connection, '获取证书列表', 'cat /usr/trim/etc/network_cert_all.conf'); + $list = json_decode($cert_all, true); + if (!$list) throw new Exception('获取证书列表失败'); + + $success = 0; + foreach ($list as $row) { + if (empty($row['san'])) continue; + $cert_domains = $row['san']; + $flag = false; + foreach ($cert_domains as $domain) { + if (in_array($domain, $domains)) { + $flag = true; + break; + } + } + if ($flag) { + $certPath = $row['certificate']; + $keyPath = $row['privateKey']; + $certDir = dirname($certPath); + $this->exec($connection, '上传证书文件', "sudo tee ".$certPath." > /dev/null <<'EOF'\n".$fullchain."\nEOF"); + $this->exec($connection, '上传私钥文件', "sudo tee ".$keyPath." > /dev/null <<'EOF'\n".$privatekey."\nEOF"); + $this->exec($connection, '刷新目录权限', 'sudo chmod 0755 "'.$certDir.'" -R'); + $this->exec($connection, '更新数据表', 'sudo -u postgres psql -d trim_connect -c "UPDATE cert SET valid_to='.$certInfo['validTo_time_t'].'000,valid_from='.$certInfo['validFrom_time_t'].'000,issued_by=\''.$certInfo['issuer']['CN'].'\',updated_time='.getMillisecond().' WHERE private_key=\''.$keyPath.'\'"'); + $this->log('证书 '.$row['domain'].' 更新成功'); + $success++; + } + } + if ($success == 0) { + throw new Exception('没有要更新的证书'); + } else { + $this->exec($connection, '重启webdav', 'sudo systemctl restart webdav.service'); + $this->exec($connection, '重启smbftpd', 'sudo systemctl restart smbftpd.service'); + $this->exec($connection, '重启trim_nginx', 'sudo systemctl restart trim_nginx.service'); + } + } + + private function exec($connection, $name, $cmd) + { + $stream = ssh2_exec($connection, $cmd); + $errorStream = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR); + if (!$stream || !$errorStream) { + throw new Exception($name.'执行命令失败'); + } + stream_set_blocking($stream, true); + stream_set_blocking($errorStream, true); + $output = stream_get_contents($stream); + $errorOutput = stream_get_contents($errorStream); + fclose($stream); + fclose($errorStream); + if (trim($errorOutput)) { + if (strpos($errorOutput, 'a password is required') !== false) { + throw new Exception('权限不足,请先配置 sudo 免密'); + } + throw new Exception($name.'失败:' . trim($errorOutput)); + } else { + if (strlen($output) > 200) { + return $output; + } + $this->log($name.'成功 ' . trim($output)); + return $output; + } + } + + private function connect() + { + if (!function_exists('ssh2_connect')) { + throw new Exception('ssh2扩展未安装'); + } + if (empty($this->config['host']) || empty($this->config['port']) || empty($this->config['username']) || empty($this->config['password'])) { + throw new Exception('必填参数不能为空'); + } + if (!filter_var($this->config['host'], FILTER_VALIDATE_IP) && !filter_var($this->config['host'], FILTER_VALIDATE_DOMAIN)) { + throw new Exception('主机地址不合法'); + } + if (!is_numeric($this->config['port']) || $this->config['port'] < 1 || $this->config['port'] > 65535) { + throw new Exception('SSH端口不合法'); + } + + $connection = ssh2_connect($this->config['host'], intval($this->config['port'])); + if (!$connection) { + throw new Exception('SSH连接失败'); + } + if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) { + throw new Exception('用户名或密码错误'); + } + return $connection; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/ftp.php b/app/lib/deploy/ftp.php index cfff989..dcabd50 100644 --- a/app/lib/deploy/ftp.php +++ b/app/lib/deploy/ftp.php @@ -1,113 +1,113 @@ -config = $config; - } - - public function check() - { - $this->connect(); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $conn_id = $this->connect(); - ftp_pasv($conn_id, true); - if ($config['format'] == 'pem') { - $temp_stream = fopen('php://temp', 'r+'); - fwrite($temp_stream, $fullchain); - rewind($temp_stream); - if (ftp_fput($conn_id, $config['pem_cert_file'], $temp_stream, FTP_BINARY)) { - $this->log('证书文件上传成功:' . $config['pem_cert_file']); - } else { - fclose($temp_stream); - ftp_close($conn_id); - throw new Exception('证书文件上传失败:' . $config['pem_cert_file']); - } - fclose($temp_stream); - - $temp_stream = fopen('php://temp', 'r+'); - fwrite($temp_stream, $privatekey); - rewind($temp_stream); - if (ftp_fput($conn_id, $config['pem_key_file'], $temp_stream, FTP_BINARY)) { - $this->log('私钥文件上传成功:' . $config['pem_key_file']); - } else { - fclose($temp_stream); - ftp_close($conn_id); - throw new Exception('私钥文件上传失败:' . $config['pem_key_file']); - } - fclose($temp_stream); - } elseif ($config['format'] == 'pfx') { - $pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null); - - $temp_stream = fopen('php://temp', 'r+'); - fwrite($temp_stream, $pfx); - rewind($temp_stream); - if (ftp_fput($conn_id, $config['pfx_file'], $temp_stream, FTP_BINARY)) { - $this->log('PFX证书文件上传成功:' . $config['pfx_file']); - } else { - fclose($temp_stream); - ftp_close($conn_id); - throw new Exception('PFX证书文件上传失败:' . $config['pfx_file']); - } - fclose($temp_stream); - } - ftp_close($conn_id); - } - - private function connect() - { - if (!function_exists('ftp_connect')) { - throw new Exception('ftp扩展未安装'); - } - if (empty($this->config['host']) || empty($this->config['port']) || empty($this->config['username']) || empty($this->config['password'])) { - throw new Exception('必填参数不能为空'); - } - if (!filter_var($this->config['host'], FILTER_VALIDATE_IP) && !filter_var($this->config['host'], FILTER_VALIDATE_DOMAIN)) { - throw new Exception('主机地址不合法'); - } - if (!is_numeric($this->config['port']) || $this->config['port'] < 1 || $this->config['port'] > 65535) { - throw new Exception('端口不合法'); - } - - if ($this->config['secure'] == '1') { - $conn_id = ftp_ssl_connect($this->config['host'], intval($this->config['port']), 10); - if (!$conn_id) { - throw new Exception('FTP服务器无法连接(SSL)'); - } - } else { - $conn_id = ftp_connect($this->config['host'], intval($this->config['port']), 10); - if (!$conn_id) { - throw new Exception('FTP服务器无法连接'); - } - } - if (!ftp_login($conn_id, $this->config['username'], $this->config['password'])) { - ftp_close($conn_id); - throw new Exception('FTP登录失败'); - } - return $conn_id; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - public function setLogger($logger) - { - $this->logger = $logger; - } -} +config = $config; + } + + public function check() + { + $this->connect(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $conn_id = $this->connect(); + ftp_pasv($conn_id, true); + if ($config['format'] == 'pem') { + $temp_stream = fopen('php://temp', 'r+'); + fwrite($temp_stream, $fullchain); + rewind($temp_stream); + if (ftp_fput($conn_id, $config['pem_cert_file'], $temp_stream, FTP_BINARY)) { + $this->log('证书文件上传成功:' . $config['pem_cert_file']); + } else { + fclose($temp_stream); + ftp_close($conn_id); + throw new Exception('证书文件上传失败:' . $config['pem_cert_file']); + } + fclose($temp_stream); + + $temp_stream = fopen('php://temp', 'r+'); + fwrite($temp_stream, $privatekey); + rewind($temp_stream); + if (ftp_fput($conn_id, $config['pem_key_file'], $temp_stream, FTP_BINARY)) { + $this->log('私钥文件上传成功:' . $config['pem_key_file']); + } else { + fclose($temp_stream); + ftp_close($conn_id); + throw new Exception('私钥文件上传失败:' . $config['pem_key_file']); + } + fclose($temp_stream); + } elseif ($config['format'] == 'pfx') { + $pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null); + + $temp_stream = fopen('php://temp', 'r+'); + fwrite($temp_stream, $pfx); + rewind($temp_stream); + if (ftp_fput($conn_id, $config['pfx_file'], $temp_stream, FTP_BINARY)) { + $this->log('PFX证书文件上传成功:' . $config['pfx_file']); + } else { + fclose($temp_stream); + ftp_close($conn_id); + throw new Exception('PFX证书文件上传失败:' . $config['pfx_file']); + } + fclose($temp_stream); + } + ftp_close($conn_id); + } + + private function connect() + { + if (!function_exists('ftp_connect')) { + throw new Exception('ftp扩展未安装'); + } + if (empty($this->config['host']) || empty($this->config['port']) || empty($this->config['username']) || empty($this->config['password'])) { + throw new Exception('必填参数不能为空'); + } + if (!filter_var($this->config['host'], FILTER_VALIDATE_IP) && !filter_var($this->config['host'], FILTER_VALIDATE_DOMAIN)) { + throw new Exception('主机地址不合法'); + } + if (!is_numeric($this->config['port']) || $this->config['port'] < 1 || $this->config['port'] > 65535) { + throw new Exception('端口不合法'); + } + + if ($this->config['secure'] == '1') { + $conn_id = ftp_ssl_connect($this->config['host'], intval($this->config['port']), 10); + if (!$conn_id) { + throw new Exception('FTP服务器无法连接(SSL)'); + } + } else { + $conn_id = ftp_connect($this->config['host'], intval($this->config['port']), 10); + if (!$conn_id) { + throw new Exception('FTP服务器无法连接'); + } + } + if (!ftp_login($conn_id, $this->config['username'], $this->config['password'])) { + ftp_close($conn_id); + throw new Exception('FTP登录失败'); + } + return $conn_id; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + public function setLogger($logger) + { + $this->logger = $logger; + } +} diff --git a/app/lib/deploy/gcore.php b/app/lib/deploy/gcore.php index a779d56..ce565fe 100644 --- a/app/lib/deploy/gcore.php +++ b/app/lib/deploy/gcore.php @@ -1,77 +1,77 @@ -apikey = $config['apikey']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->apikey)) throw new Exception('API令牌不能为空'); - $this->request('/iam/clients/me'); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $id = $config['id']; - if (empty($id)) throw new Exception('证书ID不能为空'); - - $params = [ - 'name' => $config['name'], - 'sslCertificate' => $fullchain, - 'sslPrivateKey' => $privatekey, - 'validate_root_ca' => true, - ]; - $this->request('/cdn/sslData/' . $id, $params, 'PUT'); - $this->log('证书ID:' . $id . '更新成功!'); - } - - private function request($path, $params = null, $method = null) - { - $url = $this->url . $path; - $headers = ['Authorization' => 'APIKey ' . $this->apikey]; - $body = null; - if ($params) { - $headers['Content-Type'] = 'application/json'; - $body = json_encode($params); - } - $response = http_request($url, $body, null, null, $headers, $this->proxy, $method); - $result = json_decode($response['body'], true); - if ($response['code'] >= 200 && $response['code'] < 300) { - return $result; - } elseif (isset($result['message']['message'])) { - throw new Exception($result['message']['message']); - } elseif (isset($result['errors'])) { - $errors = $result['errors'][array_key_first($result['errors'])]; - throw new Exception($errors[0]); - } else { - if (!empty($response['body'])) $this->log('Response:' . $response['body']); - throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +apikey = $config['apikey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->apikey)) throw new Exception('API令牌不能为空'); + $this->request('/iam/clients/me'); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $id = $config['id']; + if (empty($id)) throw new Exception('证书ID不能为空'); + + $params = [ + 'name' => $config['name'], + 'sslCertificate' => $fullchain, + 'sslPrivateKey' => $privatekey, + 'validate_root_ca' => true, + ]; + $this->request('/cdn/sslData/' . $id, $params, 'PUT'); + $this->log('证书ID:' . $id . '更新成功!'); + } + + private function request($path, $params = null, $method = null) + { + $url = $this->url . $path; + $headers = ['Authorization' => 'APIKey ' . $this->apikey]; + $body = null; + if ($params) { + $headers['Content-Type'] = 'application/json'; + $body = json_encode($params); + } + $response = http_request($url, $body, null, null, $headers, $this->proxy, $method); + $result = json_decode($response['body'], true); + if ($response['code'] >= 200 && $response['code'] < 300) { + return $result; + } elseif (isset($result['message']['message'])) { + throw new Exception($result['message']['message']); + } elseif (isset($result['errors'])) { + $errors = $result['errors'][array_key_first($result['errors'])]; + throw new Exception($errors[0]); + } else { + if (!empty($response['body'])) $this->log('Response:' . $response['body']); + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/goedge.php b/app/lib/deploy/goedge.php index 0bbcdcd..a9bac2d 100644 --- a/app/lib/deploy/goedge.php +++ b/app/lib/deploy/goedge.php @@ -1,154 +1,154 @@ -url = rtrim($config['url'], '/'); - $this->accessKeyId = $config['accessKeyId']; - $this->accessKey = $config['accessKey']; - $this->usertype = $config['usertype']; - $this->systype = $config['systype']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->accessKeyId) || empty($this->accessKey)) throw new Exception('必填参数不能为空'); - $this->getAccessToken(); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $domains = $config['domainList']; - if (empty($domains)) throw new Exception('没有设置要部署的域名'); - - $this->getAccessToken(); - - $params = [ - 'domains' => $domains, - 'offset' => 0, - 'size' => 10, - ]; - try { - $data = $this->request('/SSLCertService/listSSLCerts', $params); - } catch (Exception $e) { - throw new Exception('获取证书列表失败:' . $e->getMessage()); - } - $list = json_decode(base64_decode($data['sslCertsJSON']), true); - if ($list === false) { - throw new Exception('证书列表为空'); - } - $this->log('获取证书列表成功(total=' . count($list) . ')'); - - $certInfo = openssl_x509_parse($fullchain, true); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - if (!empty($list)) { - foreach ($list as $row) { - $params = [ - 'sslCertId' => $row['id'], - 'isOn' => true, - 'name' => $row['name'], - 'description' => $row['description'], - 'serverName' => $row['serverName'], - 'isCA' => false, - 'certData' => base64_encode($fullchain), - 'keyData' => base64_encode($privatekey), - 'timeBeginAt' => $certInfo['validFrom_time_t'], - 'timeEndAt' => $certInfo['validTo_time_t'], - 'dnsNames' => $domains, - 'commonNames' => [$certInfo['issuer']['CN']], - ]; - $this->request('/SSLCertService/updateSSLCert', $params); - $this->log('证书ID:' . $row['id'] . '更新成功!'); - } - } else { - $params = [ - 'isOn' => true, - 'name' => $cert_name, - 'description' => $cert_name, - 'serverName' => $certInfo['subject']['CN'], - 'isCA' => false, - 'certData' => base64_encode($fullchain), - 'keyData' => base64_encode($privatekey), - 'timeBeginAt' => $certInfo['validFrom_time_t'], - 'timeEndAt' => $certInfo['validTo_time_t'], - 'dnsNames' => $domains, - 'commonNames' => [$certInfo['issuer']['CN']], - ]; - $result = $this->request('/SSLCertService/createSSLCert', $params); - $this->log('证书ID:' . $result['sslCertId'] . '添加成功!'); - } - } - - private function getAccessToken() - { - $path = '/APIAccessTokenService/getAPIAccessToken'; - $params = [ - 'type' => $this->usertype, - 'accessKeyId' => $this->accessKeyId, - 'accessKey' => $this->accessKey, - ]; - $result = $this->request($path, $params); - if (isset($result['token'])) { - $this->accessToken = $result['token']; - } else { - throw new Exception('登录成功,获取AccessToken失败'); - } - } - - private function request($path, $params = null) - { - $url = $this->url . $path; - $headers = []; - $body = null; - if ($this->accessToken) { - if ($this->systype == '1') { - $headers['X-Cloud-Access-Token'] = $this->accessToken; - } else { - $headers['X-Edge-Access-Token'] = $this->accessToken; - } - } - if ($params) { - $headers['Content-Type'] = 'application/json'; - $body = json_encode($params); - } - $response = http_request($url, $body, null, null, $headers, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['code']) && $result['code'] == 200) { - return $result['data'] ?? null; - } elseif (isset($result['message'])) { - throw new Exception($result['message']); - } else { - if (!empty($response['body'])) $this->log('Response:' . $response['body']); - throw new Exception('返回数据解析失败'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +url = rtrim($config['url'], '/'); + $this->accessKeyId = $config['accessKeyId']; + $this->accessKey = $config['accessKey']; + $this->usertype = $config['usertype']; + $this->systype = $config['systype']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->accessKeyId) || empty($this->accessKey)) throw new Exception('必填参数不能为空'); + $this->getAccessToken(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domains = $config['domainList']; + if (empty($domains)) throw new Exception('没有设置要部署的域名'); + + $this->getAccessToken(); + + $params = [ + 'domains' => $domains, + 'offset' => 0, + 'size' => 10, + ]; + try { + $data = $this->request('/SSLCertService/listSSLCerts', $params); + } catch (Exception $e) { + throw new Exception('获取证书列表失败:' . $e->getMessage()); + } + $list = json_decode(base64_decode($data['sslCertsJSON']), true); + if ($list === false) { + throw new Exception('证书列表为空'); + } + $this->log('获取证书列表成功(total=' . count($list) . ')'); + + $certInfo = openssl_x509_parse($fullchain, true); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + if (!empty($list)) { + foreach ($list as $row) { + $params = [ + 'sslCertId' => $row['id'], + 'isOn' => true, + 'name' => $row['name'], + 'description' => $row['description'], + 'serverName' => $row['serverName'], + 'isCA' => false, + 'certData' => base64_encode($fullchain), + 'keyData' => base64_encode($privatekey), + 'timeBeginAt' => $certInfo['validFrom_time_t'], + 'timeEndAt' => $certInfo['validTo_time_t'], + 'dnsNames' => $domains, + 'commonNames' => [$certInfo['issuer']['CN']], + ]; + $this->request('/SSLCertService/updateSSLCert', $params); + $this->log('证书ID:' . $row['id'] . '更新成功!'); + } + } else { + $params = [ + 'isOn' => true, + 'name' => $cert_name, + 'description' => $cert_name, + 'serverName' => $certInfo['subject']['CN'], + 'isCA' => false, + 'certData' => base64_encode($fullchain), + 'keyData' => base64_encode($privatekey), + 'timeBeginAt' => $certInfo['validFrom_time_t'], + 'timeEndAt' => $certInfo['validTo_time_t'], + 'dnsNames' => $domains, + 'commonNames' => [$certInfo['issuer']['CN']], + ]; + $result = $this->request('/SSLCertService/createSSLCert', $params); + $this->log('证书ID:' . $result['sslCertId'] . '添加成功!'); + } + } + + private function getAccessToken() + { + $path = '/APIAccessTokenService/getAPIAccessToken'; + $params = [ + 'type' => $this->usertype, + 'accessKeyId' => $this->accessKeyId, + 'accessKey' => $this->accessKey, + ]; + $result = $this->request($path, $params); + if (isset($result['token'])) { + $this->accessToken = $result['token']; + } else { + throw new Exception('登录成功,获取AccessToken失败'); + } + } + + private function request($path, $params = null) + { + $url = $this->url . $path; + $headers = []; + $body = null; + if ($this->accessToken) { + if ($this->systype == '1') { + $headers['X-Cloud-Access-Token'] = $this->accessToken; + } else { + $headers['X-Edge-Access-Token'] = $this->accessToken; + } + } + if ($params) { + $headers['Content-Type'] = 'application/json'; + $body = json_encode($params); + } + $response = http_request($url, $body, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 200) { + return $result['data'] ?? null; + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + if (!empty($response['body'])) $this->log('Response:' . $response['body']); + throw new Exception('返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/huawei.php b/app/lib/deploy/huawei.php index 3a38523..7db5174 100644 --- a/app/lib/deploy/huawei.php +++ b/app/lib/deploy/huawei.php @@ -1,152 +1,152 @@ -AccessKeyId = $config['AccessKeyId']; - $this->SecretAccessKey = $config['SecretAccessKey']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - } - - public function check() - { - if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); - $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, 'scm.cn-north-4.myhuaweicloud.com', $this->proxy); - $client->request('GET', '/v3/scm/certificates'); - return true; - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - if ($config['product'] == 'cdn') { - $this->deploy_cdn($fullchain, $privatekey, $config); - } elseif ($config['product'] == 'elb') { - $this->deploy_elb($fullchain, $privatekey, $config); - } elseif ($config['product'] == 'waf') { - $this->deploy_waf($fullchain, $privatekey, $config); - } - } - - private function deploy_cdn($fullchain, $privatekey, $config) - { - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, 'cdn.myhuaweicloud.com', $this->proxy); - $param = [ - 'configs' => [ - 'https' => [ - 'https_status' => 'on', - 'certificate_type' => 'server', - 'certificate_source' => 0, - 'certificate_name' => $config['cert_name'], - 'certificate_value' => $fullchain, - 'private_key' => $privatekey, - ], - ], - ]; - foreach (explode(',', $config['domain']) as $domain) { - if (empty($domain)) continue; - $client->request('PUT', '/v1.1/cdn/configuration/domains/' . $domain . '/configs', null, $param); - $this->log('CDN域名 ' . $domain . ' 部署证书成功!'); - } - } - - private function deploy_elb($fullchain, $privatekey, $config) - { - if (empty($config['project_id'])) throw new Exception('项目ID不能为空'); - if (empty($config['region_id'])) throw new Exception('区域ID不能为空'); - if (empty($config['cert_id'])) throw new Exception('证书ID不能为空'); - $endpoint = 'elb.' . $config['region_id'] . '.myhuaweicloud.com'; - $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, $endpoint, $this->proxy); - try { - $data = $client->request('GET', '/v3/' . $config['project_id'] . '/elb/certificates/' . $config['cert_id']); - } catch (Exception $e) { - throw new Exception('证书详情查询失败:' . $e->getMessage()); - } - if (isset($data['certificate']['certificate']) && trim($data['certificate']['certificate']) == trim($fullchain)) { - $this->log('ELB证书ID ' . $config['cert_id'] . ' 已存在,无需重复部署'); - return; - } - $param = [ - 'certificate' => [ - 'certificate' => $fullchain, - 'private_key' => $privatekey, - 'domain' => implode(',', $config['domainList']), - ], - ]; - $client->request('PUT', '/v3/' . $config['project_id'] . '/elb/certificates/' . $config['cert_id'], null, $param); - $this->log('ELB证书ID ' . $config['cert_id'] . ' 更新证书成功!'); - } - - private function deploy_waf($fullchain, $privatekey, $config) - { - if (empty($config['project_id'])) throw new Exception('项目ID不能为空'); - if (empty($config['region_id'])) throw new Exception('区域ID不能为空'); - if (empty($config['cert_id'])) throw new Exception('证书ID不能为空'); - $endpoint = 'waf.' . $config['region_id'] . '.myhuaweicloud.com'; - $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, $endpoint, $this->proxy); - try { - $data = $client->request('GET', '/v1/' . $config['project_id'] . '/waf/certificates/' . $config['cert_id']); - } catch (Exception $e) { - throw new Exception('证书详情查询失败:' . $e->getMessage()); - } - if (isset($data['content']) && trim($data['content']) == trim($fullchain)) { - $this->log('WAF证书ID ' . $config['cert_id'] . ' 已存在,无需重复部署'); - return; - } - $param = [ - 'name' => $config['cert_name'], - 'content' => $fullchain, - 'key' => $privatekey, - ]; - $client->request('PUT', '/v1/' . $config['project_id'] . '/waf/certificates/' . $config['cert_id'], null, $param); - $this->log('WAF证书ID ' . $config['cert_id'] . ' 更新证书成功!'); - } - - private function get_cert_id($fullchain, $privatekey) - { - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, 'scm.cn-north-4.myhuaweicloud.com', $this->proxy); - $param = [ - 'name' => $cert_name, - 'certificate' => $fullchain, - 'private_key' => $privatekey, - ]; - try { - $data = $client->request('POST', '/v3/scm/certificates/import', null, $param); - } catch (Exception $e) { - throw new Exception('上传证书失败:' . $e->getMessage()); - } - $this->log('上传证书成功 certificate_id=' . $data['certificate_id']); - return $data['certificate_id']; - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, 'scm.cn-north-4.myhuaweicloud.com', $this->proxy); + $client->request('GET', '/v3/scm/certificates'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + if ($config['product'] == 'cdn') { + $this->deploy_cdn($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'elb') { + $this->deploy_elb($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'waf') { + $this->deploy_waf($fullchain, $privatekey, $config); + } + } + + private function deploy_cdn($fullchain, $privatekey, $config) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, 'cdn.myhuaweicloud.com', $this->proxy); + $param = [ + 'configs' => [ + 'https' => [ + 'https_status' => 'on', + 'certificate_type' => 'server', + 'certificate_source' => 0, + 'certificate_name' => $config['cert_name'], + 'certificate_value' => $fullchain, + 'private_key' => $privatekey, + ], + ], + ]; + foreach (explode(',', $config['domain']) as $domain) { + if (empty($domain)) continue; + $client->request('PUT', '/v1.1/cdn/configuration/domains/' . $domain . '/configs', null, $param); + $this->log('CDN域名 ' . $domain . ' 部署证书成功!'); + } + } + + private function deploy_elb($fullchain, $privatekey, $config) + { + if (empty($config['project_id'])) throw new Exception('项目ID不能为空'); + if (empty($config['region_id'])) throw new Exception('区域ID不能为空'); + if (empty($config['cert_id'])) throw new Exception('证书ID不能为空'); + $endpoint = 'elb.' . $config['region_id'] . '.myhuaweicloud.com'; + $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, $endpoint, $this->proxy); + try { + $data = $client->request('GET', '/v3/' . $config['project_id'] . '/elb/certificates/' . $config['cert_id']); + } catch (Exception $e) { + throw new Exception('证书详情查询失败:' . $e->getMessage()); + } + if (isset($data['certificate']['certificate']) && trim($data['certificate']['certificate']) == trim($fullchain)) { + $this->log('ELB证书ID ' . $config['cert_id'] . ' 已存在,无需重复部署'); + return; + } + $param = [ + 'certificate' => [ + 'certificate' => $fullchain, + 'private_key' => $privatekey, + 'domain' => implode(',', $config['domainList']), + ], + ]; + $client->request('PUT', '/v3/' . $config['project_id'] . '/elb/certificates/' . $config['cert_id'], null, $param); + $this->log('ELB证书ID ' . $config['cert_id'] . ' 更新证书成功!'); + } + + private function deploy_waf($fullchain, $privatekey, $config) + { + if (empty($config['project_id'])) throw new Exception('项目ID不能为空'); + if (empty($config['region_id'])) throw new Exception('区域ID不能为空'); + if (empty($config['cert_id'])) throw new Exception('证书ID不能为空'); + $endpoint = 'waf.' . $config['region_id'] . '.myhuaweicloud.com'; + $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, $endpoint, $this->proxy); + try { + $data = $client->request('GET', '/v1/' . $config['project_id'] . '/waf/certificates/' . $config['cert_id']); + } catch (Exception $e) { + throw new Exception('证书详情查询失败:' . $e->getMessage()); + } + if (isset($data['content']) && trim($data['content']) == trim($fullchain)) { + $this->log('WAF证书ID ' . $config['cert_id'] . ' 已存在,无需重复部署'); + return; + } + $param = [ + 'name' => $config['cert_name'], + 'content' => $fullchain, + 'key' => $privatekey, + ]; + $client->request('PUT', '/v1/' . $config['project_id'] . '/waf/certificates/' . $config['cert_id'], null, $param); + $this->log('WAF证书ID ' . $config['cert_id'] . ' 更新证书成功!'); + } + + private function get_cert_id($fullchain, $privatekey) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, 'scm.cn-north-4.myhuaweicloud.com', $this->proxy); + $param = [ + 'name' => $cert_name, + 'certificate' => $fullchain, + 'private_key' => $privatekey, + ]; + try { + $data = $client->request('POST', '/v3/scm/certificates/import', null, $param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $this->log('上传证书成功 certificate_id=' . $data['certificate_id']); + return $data['certificate_id']; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/huoshan.php b/app/lib/deploy/huoshan.php index dd8df10..7a416ae 100644 --- a/app/lib/deploy/huoshan.php +++ b/app/lib/deploy/huoshan.php @@ -1,228 +1,228 @@ -AccessKeyId = $config['AccessKeyId']; - $this->SecretAccessKey = $config['SecretAccessKey']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - } - - public function check() - { - if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'cdn', '2021-03-01', 'cn-north-1', $this->proxy); - $client->request('POST', 'ListCertInfo', ['Source' => 'volc_cert_center']); - return true; - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if ($config['product'] == 'live') { - $this->deploy_live($fullchain, $privatekey, $config); - } else { - $cert_id = $this->get_cert_id($fullchain, $privatekey); - if (!$cert_id) throw new Exception('获取证书ID失败'); - $info['cert_id'] = $cert_id; - if (!isset($config['product']) || $config['product'] == 'cdn') { - $this->deploy_cdn($cert_id, $config); - } elseif ($config['product'] == 'dcdn') { - $this->deploy_dcdn($cert_id, $config); - } elseif ($config['product'] == 'tos') { - $this->deploy_tos($cert_id, $config); - } elseif ($config['product'] == 'imagex') { - $this->deploy_imagex($cert_id, $config); - } elseif ($config['product'] == 'clb') { - $this->deploy_clb($cert_id, $config); - } elseif ($config['product'] == 'alb') { - $this->deploy_alb($cert_id, $config); - } - } - } - - private function deploy_cdn($cert_id, $config) - { - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'cdn.volcengineapi.com', 'cdn', '2021-03-01', 'cn-north-1', $this->proxy); - $param = [ - 'CertId' => $cert_id, - 'Domain' => $config['domain'], - ]; - $data = $client->request('POST', 'BatchDeployCert', $param); - if (empty($data['DeployResult'])) throw new Exception('部署证书失败:DeployResult为空'); - foreach ($data['DeployResult'] as $row) { - if ($row['Status'] == 'success') { - $this->log('CDN域名 ' . $row['Domain'] . ' 部署证书成功!'); - } else { - $this->log('CDN域名 ' . $row['Domain'] . ' 部署证书失败:' . (isset($row['ErrorMsg']) ? $row['ErrorMsg'] : '')); - } - } - } - - private function deploy_dcdn($cert_id, $config) - { - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'dcdn', '2021-04-01', 'cn-north-1', $this->proxy); - $param = [ - 'CertId' => $cert_id, - 'DomainNames' => explode(',', $config['domain']), - ]; - $client->request('POST', 'CreateCertBind', $param); - $this->log('DCDN域名 ' . $config['domain'] . ' 部署证书成功!'); - } - - private function deploy_tos($cert_id, $config) - { - if (empty($config['bucket_domain'])) throw new Exception('Bucket域名不能为空'); - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, $config['bucket_domain'], 'tos', '2021-04-01', 'cn-beijing', $this->proxy); - foreach (explode(',', $config['domain']) as $domain) { - if (empty($domain)) continue; - $param = [ - 'CustomDomainRule' => [ - 'Domain' => $domain, - 'CertId' => $cert_id, - ] - ]; - $query = ['customdomain' => '']; - $client->tos_request('PUT', $param, $query); - $this->log('对象存储域名 ' . $config['domain'] . ' 部署证书成功!'); - } - } - - private function deploy_live($fullchain, $privatekey, $config) - { - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'live.volcengineapi.com', 'live', '2023-01-01', 'cn-north-1', $this->proxy); - $param = [ - 'CertName' => $cert_name, - 'Rsa' => [ - 'Pubkey' => $fullchain, - 'Prikey' => $privatekey, - ], - 'UseWay' => 'https', - ]; - $result = $client->request('POST', 'CreateCert', $param); - $this->log('上传证书成功 ChainID=' . $result['ChainID']); - - foreach (explode(',', $config['domain']) as $domain) { - if (empty($domain)) continue; - $param = [ - 'ChainID' => $result['ChainID'], - 'Domain' => $domain, - 'HTTPS' => true, - 'HTTP2' => true, - ]; - $client->request('POST', 'BindCert', $param); - $this->log('视频直播域名 ' . $domain . ' 部署证书成功!'); - } - } - - private function deploy_imagex($cert_id, $config) - { - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'imagex.volcengineapi.com', 'imagex', '2018-08-01', 'cn-north-1', $this->proxy); - foreach (explode(',', $config['domain']) as $domain) { - if (empty($domain)) continue; - $param = [ - [ - 'domain' => $domain, - 'cert_id' => $cert_id, - ] - ]; - $result = $client->request('POST', 'UpdateImageBatchDomainCert', $param); - if (isset($result['SuccessDomains']) && count($result['SuccessDomains']) > 0) { - $this->log('veImageX域名 ' . $domain . ' 部署证书成功!'); - } elseif (isset($result['FailedDomains']) && count($result['FailedDomains']) > 0) { - $errmsg = $result['FailedDomains'][0]['ErrMsg']; - $this->log('veImageX域名 ' . $domain . ' 部署证书失败:' . $errmsg); - } else { - $this->log('veImageX域名 ' . $domain . ' 部署证书失败'); - } - } - } - - private function deploy_clb($cert_id, $config) - { - if (empty($config['listener_id'])) throw new Exception('监听器ID不能为空'); - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'clb', '2020-04-01', 'cn-beijing', $this->proxy); - $param = [ - 'ListenerId' => $config['listener_id'], - 'CertificateSource' => 'cert_center', - 'CertCenterCertificateId' => $cert_id, - ]; - $client->request('GET', 'ModifyListenerAttributes', $param); - $this->log('CLB监听器 ' . $config['listener_id'] . ' 部署证书成功!'); - } - - private function deploy_alb($cert_id, $config) - { - if (empty($config['listener_id'])) throw new Exception('监听器ID不能为空'); - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'alb', '2020-04-01', 'cn-beijing', $this->proxy); - $param = [ - 'ListenerId' => $config['listener_id'], - 'CertificateSource' => 'cert_center', - 'CertCenterCertificateId' => $cert_id, - ]; - $client->request('GET', 'ModifyListenerAttributes', $param); - $this->log('ALB监听器 ' . $config['listener_id'] . ' 部署证书成功!'); - } - - private function get_cert_id($fullchain, $privatekey) - { - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'certificate_service', '2024-10-01', 'cn-beijing', $this->proxy); - $param = [ - 'Tag' => $cert_name, - 'Repeatable' => false, - 'CertificateInfo' => [ - 'CertificateChain' => $fullchain, - 'PrivateKey' => $privatekey, - ], - ]; - try { - $data = $client->request('POST', 'ImportCertificate', $param); - } catch (Exception $e) { - throw new Exception('上传证书失败:' . $e->getMessage()); - } - if (!empty($data['InstanceId'])) { - $cert_id = $data['InstanceId']; - } else { - $cert_id = $data['RepeatId']; - } - $this->log('上传证书成功 CertId=' . $cert_id); - return $cert_id; - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'cdn', '2021-03-01', 'cn-north-1', $this->proxy); + $client->request('POST', 'ListCertInfo', ['Source' => 'volc_cert_center']); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if ($config['product'] == 'live') { + $this->deploy_live($fullchain, $privatekey, $config); + } else { + $cert_id = $this->get_cert_id($fullchain, $privatekey); + if (!$cert_id) throw new Exception('获取证书ID失败'); + $info['cert_id'] = $cert_id; + if (!isset($config['product']) || $config['product'] == 'cdn') { + $this->deploy_cdn($cert_id, $config); + } elseif ($config['product'] == 'dcdn') { + $this->deploy_dcdn($cert_id, $config); + } elseif ($config['product'] == 'tos') { + $this->deploy_tos($cert_id, $config); + } elseif ($config['product'] == 'imagex') { + $this->deploy_imagex($cert_id, $config); + } elseif ($config['product'] == 'clb') { + $this->deploy_clb($cert_id, $config); + } elseif ($config['product'] == 'alb') { + $this->deploy_alb($cert_id, $config); + } + } + } + + private function deploy_cdn($cert_id, $config) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'cdn.volcengineapi.com', 'cdn', '2021-03-01', 'cn-north-1', $this->proxy); + $param = [ + 'CertId' => $cert_id, + 'Domain' => $config['domain'], + ]; + $data = $client->request('POST', 'BatchDeployCert', $param); + if (empty($data['DeployResult'])) throw new Exception('部署证书失败:DeployResult为空'); + foreach ($data['DeployResult'] as $row) { + if ($row['Status'] == 'success') { + $this->log('CDN域名 ' . $row['Domain'] . ' 部署证书成功!'); + } else { + $this->log('CDN域名 ' . $row['Domain'] . ' 部署证书失败:' . (isset($row['ErrorMsg']) ? $row['ErrorMsg'] : '')); + } + } + } + + private function deploy_dcdn($cert_id, $config) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'dcdn', '2021-04-01', 'cn-north-1', $this->proxy); + $param = [ + 'CertId' => $cert_id, + 'DomainNames' => explode(',', $config['domain']), + ]; + $client->request('POST', 'CreateCertBind', $param); + $this->log('DCDN域名 ' . $config['domain'] . ' 部署证书成功!'); + } + + private function deploy_tos($cert_id, $config) + { + if (empty($config['bucket_domain'])) throw new Exception('Bucket域名不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, $config['bucket_domain'], 'tos', '2021-04-01', 'cn-beijing', $this->proxy); + foreach (explode(',', $config['domain']) as $domain) { + if (empty($domain)) continue; + $param = [ + 'CustomDomainRule' => [ + 'Domain' => $domain, + 'CertId' => $cert_id, + ] + ]; + $query = ['customdomain' => '']; + $client->tos_request('PUT', $param, $query); + $this->log('对象存储域名 ' . $config['domain'] . ' 部署证书成功!'); + } + } + + private function deploy_live($fullchain, $privatekey, $config) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'live.volcengineapi.com', 'live', '2023-01-01', 'cn-north-1', $this->proxy); + $param = [ + 'CertName' => $cert_name, + 'Rsa' => [ + 'Pubkey' => $fullchain, + 'Prikey' => $privatekey, + ], + 'UseWay' => 'https', + ]; + $result = $client->request('POST', 'CreateCert', $param); + $this->log('上传证书成功 ChainID=' . $result['ChainID']); + + foreach (explode(',', $config['domain']) as $domain) { + if (empty($domain)) continue; + $param = [ + 'ChainID' => $result['ChainID'], + 'Domain' => $domain, + 'HTTPS' => true, + 'HTTP2' => true, + ]; + $client->request('POST', 'BindCert', $param); + $this->log('视频直播域名 ' . $domain . ' 部署证书成功!'); + } + } + + private function deploy_imagex($cert_id, $config) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'imagex.volcengineapi.com', 'imagex', '2018-08-01', 'cn-north-1', $this->proxy); + foreach (explode(',', $config['domain']) as $domain) { + if (empty($domain)) continue; + $param = [ + [ + 'domain' => $domain, + 'cert_id' => $cert_id, + ] + ]; + $result = $client->request('POST', 'UpdateImageBatchDomainCert', $param); + if (isset($result['SuccessDomains']) && count($result['SuccessDomains']) > 0) { + $this->log('veImageX域名 ' . $domain . ' 部署证书成功!'); + } elseif (isset($result['FailedDomains']) && count($result['FailedDomains']) > 0) { + $errmsg = $result['FailedDomains'][0]['ErrMsg']; + $this->log('veImageX域名 ' . $domain . ' 部署证书失败:' . $errmsg); + } else { + $this->log('veImageX域名 ' . $domain . ' 部署证书失败'); + } + } + } + + private function deploy_clb($cert_id, $config) + { + if (empty($config['listener_id'])) throw new Exception('监听器ID不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'clb', '2020-04-01', 'cn-beijing', $this->proxy); + $param = [ + 'ListenerId' => $config['listener_id'], + 'CertificateSource' => 'cert_center', + 'CertCenterCertificateId' => $cert_id, + ]; + $client->request('GET', 'ModifyListenerAttributes', $param); + $this->log('CLB监听器 ' . $config['listener_id'] . ' 部署证书成功!'); + } + + private function deploy_alb($cert_id, $config) + { + if (empty($config['listener_id'])) throw new Exception('监听器ID不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'alb', '2020-04-01', 'cn-beijing', $this->proxy); + $param = [ + 'ListenerId' => $config['listener_id'], + 'CertificateSource' => 'cert_center', + 'CertCenterCertificateId' => $cert_id, + ]; + $client->request('GET', 'ModifyListenerAttributes', $param); + $this->log('ALB监听器 ' . $config['listener_id'] . ' 部署证书成功!'); + } + + private function get_cert_id($fullchain, $privatekey) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'certificate_service', '2024-10-01', 'cn-beijing', $this->proxy); + $param = [ + 'Tag' => $cert_name, + 'Repeatable' => false, + 'CertificateInfo' => [ + 'CertificateChain' => $fullchain, + 'PrivateKey' => $privatekey, + ], + ]; + try { + $data = $client->request('POST', 'ImportCertificate', $param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + if (!empty($data['InstanceId'])) { + $cert_id = $data['InstanceId']; + } else { + $cert_id = $data['RepeatId']; + } + $this->log('上传证书成功 CertId=' . $cert_id); + return $cert_id; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/k8s.php b/app/lib/deploy/k8s.php index c69d2bc..a67830e 100644 --- a/app/lib/deploy/k8s.php +++ b/app/lib/deploy/k8s.php @@ -1,202 +1,202 @@ -kubeconfig = $config['kubeconfig']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->kubeconfig)) throw new Exception('Kubeconfig不能为空'); - $this->verify(); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $namespace = $config['namespace']; - $secretName = $config['secret_name']; - if (empty($namespace)) throw new Exception('命名空间不能为空'); - if (empty($secretName)) throw new Exception('Secret名称不能为空'); - - $this->parse(); - - $secretPayload = [ - 'apiVersion' => 'v1', - 'kind' => 'Secret', - 'metadata' => ['name' => $secretName, 'namespace' => $namespace], - 'type' => 'kubernetes.io/tls', - 'data' => [ - 'tls.crt' => base64_encode($config['fullchain']), - 'tls.key' => base64_encode($config['privatekey']), - ], - ]; - - $secretUrl = '/api/v1/namespaces/' . $namespace . '/secrets/' . $secretName; - list($sCode, $sBody, $sErr) = $this->k8s_request('GET', $secretUrl); - - if ($sCode === 404) { - $createUrl = '/api/v1/namespaces/' . $namespace . '/secrets'; - $this->log('Secret:' . $secretName . ' 不存在,正在创建...'); - list($cCode, $cBody, $cErr) = $this->k8s_request('POST', $createUrl, json_encode($secretPayload)); - if ($cCode < 200 || $cCode >= 300) throw new Exception("创建Secret失败 (HTTP $cCode): $cBody | $cErr"); - $this->log('Secret:' . $namespace . ' 创建成功'); - } elseif ($sCode >= 200 && $sCode < 300) { - $this->log('Secret:' . $secretName . ' 已存在,正在更新...'); - $patch = ['data' => $secretPayload['data'], 'type' => 'kubernetes.io/tls']; - list($pCode, $pBody, $pErr) = $this->k8s_request('PATCH', $secretUrl, json_encode($patch)); - if ($pCode < 200 || $pCode >= 300) throw new Exception("更新Secret失败 (HTTP $pCode): $pBody | $pErr"); - $this->log('Secret:' . $secretName . ' 更新成功'); - } else { - throw new Exception("获取Secret失败 (HTTP $sCode): $sBody | $sErr"); - } - - // Bind Secret to specified Ingresses (merge spec.tls & hosts) ---- - if (!empty($config['ingresses'])) { - $ingressUrl = '/apis/networking.k8s.io/v1/namespaces/' . $namespace . '/ingresses'; - foreach (explode(',', $config['ingresses']) as $ingName) { - list($gCode, $gBody, $gErr) = $this->k8s_request('GET', $ingressUrl . '/' . $ingName); - if ($gCode < 200 || $gCode >= 300) throw new Exception("获取Ingress '$ingName' 失败 (HTTP $gCode): $gBody | $gErr"); - $ing = json_decode($gBody, true); - if (!$ing) throw new Exception("解析Ingress '$ingName' JSON失败: $gBody"); - - // collect hosts from spec.rules - $hosts = []; - foreach (($ing['spec']['rules'] ?? []) as $rule) { - if (!empty($rule['host'])) $hosts[] = $rule['host']; - } - $hosts = array_values(array_unique($hosts)); - - // merge/ensure spec.tls entry - $tls = $ing['spec']['tls'] ?? []; - $found = false; - foreach ($tls as &$entry) { - if (($entry['secretName'] ?? '') === $secretName) { - $found = true; - $existingHosts = $entry['hosts'] ?? []; - $entry['hosts'] = array_values(array_unique(array_merge($existingHosts, $hosts))); - } - } - unset($entry); - if (!$found) { - $tls[] = ['secretName' => $secretName, 'hosts' => $hosts]; - } - - $patch = ['spec' => ['tls' => $tls]]; - list($iCode, $iBody, $iErr) = $this->k8s_request('PATCH', $ingressUrl . '/' . $ingName, json_encode($patch, JSON_UNESCAPED_SLASHES)); - if ($iCode < 200 || $iCode >= 300) throw new Exception("更新Ingress '$ingName' 失败 (HTTP $iCode): $iBody | $iErr"); - $this->log("Ingress '$ingName' 更新TLS成功"); - } - } - } - - private function parse() - { - $kcfg = Yaml::parse($this->kubeconfig); - if (!$kcfg) throw new Exception('Kubeconfig格式错误'); - $curr = $kcfg['current-context'] ?? null; - if (!$curr) throw new Exception('Kubeconfig缺少current-context'); - - $contexts = $this->index_by_name($kcfg['contexts'] ?? []); - $clusters = $this->index_by_name($kcfg['clusters'] ?? []); - $users = $this->index_by_name($kcfg['users'] ?? []); - - $ctx = $contexts[$curr] ?? null; - if (!$ctx) throw new Exception("Kubeconfig中找不到current-context: $curr"); - - $clusterName = $ctx['context']['cluster'] ?? null; - $userName = $ctx['context']['user'] ?? null; - if (!$clusterName || !$userName) throw new Exception("Kubeconfig中context缺少cluster或user: $curr"); - - $cluster = $clusters[$clusterName] ?? null; - $user = $users[$userName] ?? null; - if (!$cluster) throw new Exception("Kubeconfig中找不到cluster: $clusterName"); - if (!$user) throw new Exception("Kubeconfig中找不到user: $userName"); - - $this->server = $cluster['cluster']['server'] ?? null; - if (!$this->server) throw new Exception("Kubeconfig中找不到cluster.server"); - $this->server = rtrim($this->server, '/'); - - $this->bearerToken = $user['user']['token'] ?? ($user['user']['auth-provider']['config']['access-token'] ?? null); - $clientCertFile = $clientKeyFile = null; - if (!empty($user['user']['client-certificate-data']) && !empty($user['user']['client-key-data'])) { - $clientCertFile = tempnam(sys_get_temp_dir(), 'kcc_'); - $clientKeyFile = tempnam(sys_get_temp_dir(), 'kck_'); - file_put_contents($clientCertFile, base64_decode($user['user']['client-certificate-data'])); - file_put_contents($clientKeyFile, base64_decode($user['user']['client-key-data'])); - } elseif (!empty($user['user']['client-certificate']) && !empty($user['user']['client-key'])) { - $clientCertFile = $user['user']['client-certificate']; - $clientKeyFile = $user['user']['client-key']; - } - $this->tls = ['cert' => $clientCertFile, 'key' => $clientKeyFile]; - } - - private function verify() - { - $this->parse(); - list($vCode, $vBody, $vErr) = $this->k8s_request('GET', '/version'); - if ($vErr) throw new Exception("连接Kubernetes API服务器失败: $vErr"); - if ($vCode != 200) throw new Exception("连接Kubernetes API服务器失败: HTTP $vCode $vBody"); - } - - private function k8s_request($method, $path, $body = null) - { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $this->server . $path); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 30); - $headers = ['Accept: application/json']; - if ($this->bearerToken) $headers[] = 'Authorization: Bearer ' . $this->bearerToken; - if ($body !== null) $headers[] = 'Content-Type: application/json'; - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - if (!empty($this->tls['cert']) && !empty($this->tls['key'])) { - curl_setopt($ch, CURLOPT_SSLCERT, $this->tls['cert']); - curl_setopt($ch, CURLOPT_SSLKEY, $this->tls['key']); - } - $resp = curl_exec($ch); - $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - $err = curl_error($ch); - curl_close($ch); - return [$code, $resp, $err]; - } - - private function index_by_name($arr) - { - $out = []; - foreach ($arr as $item) { - if (isset($item['name'])) $out[$item['name']] = $item; - } - return $out; - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +kubeconfig = $config['kubeconfig']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->kubeconfig)) throw new Exception('Kubeconfig不能为空'); + $this->verify(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $namespace = $config['namespace']; + $secretName = $config['secret_name']; + if (empty($namespace)) throw new Exception('命名空间不能为空'); + if (empty($secretName)) throw new Exception('Secret名称不能为空'); + + $this->parse(); + + $secretPayload = [ + 'apiVersion' => 'v1', + 'kind' => 'Secret', + 'metadata' => ['name' => $secretName, 'namespace' => $namespace], + 'type' => 'kubernetes.io/tls', + 'data' => [ + 'tls.crt' => base64_encode($config['fullchain']), + 'tls.key' => base64_encode($config['privatekey']), + ], + ]; + + $secretUrl = '/api/v1/namespaces/' . $namespace . '/secrets/' . $secretName; + list($sCode, $sBody, $sErr) = $this->k8s_request('GET', $secretUrl); + + if ($sCode === 404) { + $createUrl = '/api/v1/namespaces/' . $namespace . '/secrets'; + $this->log('Secret:' . $secretName . ' 不存在,正在创建...'); + list($cCode, $cBody, $cErr) = $this->k8s_request('POST', $createUrl, json_encode($secretPayload)); + if ($cCode < 200 || $cCode >= 300) throw new Exception("创建Secret失败 (HTTP $cCode): $cBody | $cErr"); + $this->log('Secret:' . $namespace . ' 创建成功'); + } elseif ($sCode >= 200 && $sCode < 300) { + $this->log('Secret:' . $secretName . ' 已存在,正在更新...'); + $patch = ['data' => $secretPayload['data'], 'type' => 'kubernetes.io/tls']; + list($pCode, $pBody, $pErr) = $this->k8s_request('PATCH', $secretUrl, json_encode($patch)); + if ($pCode < 200 || $pCode >= 300) throw new Exception("更新Secret失败 (HTTP $pCode): $pBody | $pErr"); + $this->log('Secret:' . $secretName . ' 更新成功'); + } else { + throw new Exception("获取Secret失败 (HTTP $sCode): $sBody | $sErr"); + } + + // Bind Secret to specified Ingresses (merge spec.tls & hosts) ---- + if (!empty($config['ingresses'])) { + $ingressUrl = '/apis/networking.k8s.io/v1/namespaces/' . $namespace . '/ingresses'; + foreach (explode(',', $config['ingresses']) as $ingName) { + list($gCode, $gBody, $gErr) = $this->k8s_request('GET', $ingressUrl . '/' . $ingName); + if ($gCode < 200 || $gCode >= 300) throw new Exception("获取Ingress '$ingName' 失败 (HTTP $gCode): $gBody | $gErr"); + $ing = json_decode($gBody, true); + if (!$ing) throw new Exception("解析Ingress '$ingName' JSON失败: $gBody"); + + // collect hosts from spec.rules + $hosts = []; + foreach (($ing['spec']['rules'] ?? []) as $rule) { + if (!empty($rule['host'])) $hosts[] = $rule['host']; + } + $hosts = array_values(array_unique($hosts)); + + // merge/ensure spec.tls entry + $tls = $ing['spec']['tls'] ?? []; + $found = false; + foreach ($tls as &$entry) { + if (($entry['secretName'] ?? '') === $secretName) { + $found = true; + $existingHosts = $entry['hosts'] ?? []; + $entry['hosts'] = array_values(array_unique(array_merge($existingHosts, $hosts))); + } + } + unset($entry); + if (!$found) { + $tls[] = ['secretName' => $secretName, 'hosts' => $hosts]; + } + + $patch = ['spec' => ['tls' => $tls]]; + list($iCode, $iBody, $iErr) = $this->k8s_request('PATCH', $ingressUrl . '/' . $ingName, json_encode($patch, JSON_UNESCAPED_SLASHES)); + if ($iCode < 200 || $iCode >= 300) throw new Exception("更新Ingress '$ingName' 失败 (HTTP $iCode): $iBody | $iErr"); + $this->log("Ingress '$ingName' 更新TLS成功"); + } + } + } + + private function parse() + { + $kcfg = Yaml::parse($this->kubeconfig); + if (!$kcfg) throw new Exception('Kubeconfig格式错误'); + $curr = $kcfg['current-context'] ?? null; + if (!$curr) throw new Exception('Kubeconfig缺少current-context'); + + $contexts = $this->index_by_name($kcfg['contexts'] ?? []); + $clusters = $this->index_by_name($kcfg['clusters'] ?? []); + $users = $this->index_by_name($kcfg['users'] ?? []); + + $ctx = $contexts[$curr] ?? null; + if (!$ctx) throw new Exception("Kubeconfig中找不到current-context: $curr"); + + $clusterName = $ctx['context']['cluster'] ?? null; + $userName = $ctx['context']['user'] ?? null; + if (!$clusterName || !$userName) throw new Exception("Kubeconfig中context缺少cluster或user: $curr"); + + $cluster = $clusters[$clusterName] ?? null; + $user = $users[$userName] ?? null; + if (!$cluster) throw new Exception("Kubeconfig中找不到cluster: $clusterName"); + if (!$user) throw new Exception("Kubeconfig中找不到user: $userName"); + + $this->server = $cluster['cluster']['server'] ?? null; + if (!$this->server) throw new Exception("Kubeconfig中找不到cluster.server"); + $this->server = rtrim($this->server, '/'); + + $this->bearerToken = $user['user']['token'] ?? ($user['user']['auth-provider']['config']['access-token'] ?? null); + $clientCertFile = $clientKeyFile = null; + if (!empty($user['user']['client-certificate-data']) && !empty($user['user']['client-key-data'])) { + $clientCertFile = tempnam(sys_get_temp_dir(), 'kcc_'); + $clientKeyFile = tempnam(sys_get_temp_dir(), 'kck_'); + file_put_contents($clientCertFile, base64_decode($user['user']['client-certificate-data'])); + file_put_contents($clientKeyFile, base64_decode($user['user']['client-key-data'])); + } elseif (!empty($user['user']['client-certificate']) && !empty($user['user']['client-key'])) { + $clientCertFile = $user['user']['client-certificate']; + $clientKeyFile = $user['user']['client-key']; + } + $this->tls = ['cert' => $clientCertFile, 'key' => $clientKeyFile]; + } + + private function verify() + { + $this->parse(); + list($vCode, $vBody, $vErr) = $this->k8s_request('GET', '/version'); + if ($vErr) throw new Exception("连接Kubernetes API服务器失败: $vErr"); + if ($vCode != 200) throw new Exception("连接Kubernetes API服务器失败: HTTP $vCode $vBody"); + } + + private function k8s_request($method, $path, $body = null) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->server . $path); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + $headers = ['Accept: application/json']; + if ($this->bearerToken) $headers[] = 'Authorization: Bearer ' . $this->bearerToken; + if ($body !== null) $headers[] = 'Content-Type: application/json'; + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + if (!empty($this->tls['cert']) && !empty($this->tls['key'])) { + curl_setopt($ch, CURLOPT_SSLCERT, $this->tls['cert']); + curl_setopt($ch, CURLOPT_SSLKEY, $this->tls['key']); + } + $resp = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + $err = curl_error($ch); + curl_close($ch); + return [$code, $resp, $err]; + } + + private function index_by_name($arr) + { + $out = []; + foreach ($arr as $item) { + if (isset($item['name'])) $out[$item['name']] = $item; + } + return $out; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/kangle.php b/app/lib/deploy/kangle.php index 707490a..859882d 100644 --- a/app/lib/deploy/kangle.php +++ b/app/lib/deploy/kangle.php @@ -1,208 +1,208 @@ -url = rtrim($config['url'], '/'); - $this->auth = $config['auth']; - $this->username = $config['username']; - $this->password = $config['password']; - $this->skey = $config['skey']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->username)) throw new Exception('必填参数不能为空'); - $this->login(); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $this->login(); - $this->log('登录成功 cookie:' . $this->cookie); - $this->getMain(); - - if ($config['type'] == '1' && !empty($config['domains'])) { - $domains = explode("\n", $config['domains']); - $success = 0; - $errmsg = null; - foreach ($domains as $domain) { - $domain = trim($domain); - if (empty($domain)) continue; - try { - $this->deployDomain($domain, $fullchain, $privatekey); - $this->log("域名 {$domain} 证书部署成功"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("域名 {$domain} 证书部署失败:" . $errmsg); - } - } - if ($success == 0) { - throw new Exception($errmsg ? $errmsg : '要部署的域名不存在'); - } - } else { - $this->deployAccount($fullchain, $privatekey); - $this->log("账号级SSL证书部署成功"); - } - } - - private function deployDomain($domain, $fullchain, $privatekey) - { - $path = '/vhost/?c=ssl&a=domainSsl'; - $post = [ - 'domain' => $domain, - 'certificate' => $fullchain, - 'certificate_key' => $privatekey, - ]; - $response = http_request($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); - if (strpos($response['body'], '成功')) { - return true; - } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { - throw new Exception(htmlspecialchars($match[1])); - } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { - throw new Exception(htmlspecialchars($response['body'])); - } else { - throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); - } - } - - private function deployAccount($fullchain, $privatekey) - { - $path = '/vhost/?c=ssl&a=ssl'; - $post = [ - 'certificate' => $fullchain, - 'certificate_key' => $privatekey, - ]; - $response = http_request($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); - if (strpos($response['body'], '成功')) { - return true; - } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { - throw new Exception(htmlspecialchars($match[1])); - } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { - throw new Exception(htmlspecialchars($response['body'])); - } else { - throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); - } - } - - private function login() - { - if ($this->auth == '1') { - return $this->loginBySkey(); - } else { - return $this->loginByPwd(); - } - } - - private function loginBySkey() - { - $url = $this->url . '/vhost/index.php?c=sso&a=hello&url=' . urlencode($this->url . '/index.php?'); - $response = http_request($url, null, null, null, null, $this->proxy); - if ($response['code'] == 302 && !empty($response['redirect_url'])) { - $cookie = ''; - if (isset($response['headers']['Set-Cookie'])) { - foreach ($response['headers']['Set-Cookie'] as $val) { - $arr = explode('=', $val); - if ($arr[1] == '' || $arr[1] == 'deleted') continue; - $cookie .= $val . '; '; - } - $query = parse_url($response['redirect_url'], PHP_URL_QUERY); - parse_str($query, $params); - if (isset($params['r'])) { - $sess_key = $params['r']; - $this->loginBySkey2($cookie, $sess_key); - $this->cookie = $cookie; - return true; - } else { - throw new Exception('获取SSO凭据失败,sess_key获取失败'); - } - } else { - throw new Exception('获取SSO凭据失败,获取cookie失败'); - } - } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { - throw new Exception('获取SSO凭据失败 (' . htmlspecialchars($response['body']) . ')'); - } else { - throw new Exception('获取SSO凭据失败 (httpCode=' . $response['code'] . ')'); - } - } - - private function loginBySkey2($cookie, $sess_key) - { - $s = md5($sess_key . $this->username . $sess_key . $this->skey); - $url = $this->url . '/vhost/index.php?c=sso&a=login&name=' . $this->username . '&r=' . $sess_key . '&s=' . $s; - $response = http_request($url, null, null, $cookie, null, $this->proxy); - if ($response['code'] == 302) { - return true; - } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { - throw new Exception('SSO登录失败 (' . htmlspecialchars($response['body']) . ')'); - } else { - throw new Exception('SSO登录失败 (httpCode=' . $response['code'] . ')'); - } - } - - private function loginByPwd() - { - $referer = $this->url . '/vhost/index.php?c=session&a=loginForm'; - $url = $this->url . '/vhost/index.php?c=session&a=login'; - $post = [ - 'username' => $this->username, - 'passwd' => $this->password, - ]; - $response = http_request($url, http_build_query($post), $referer, null, null, $this->proxy); - if ($response['code'] == 302) { - $cookie = ''; - if (isset($response['headers']['Set-Cookie'])) { - foreach ($response['headers']['Set-Cookie'] as $val) { - $arr = explode('=', $val); - if ($arr[1] == '' || $arr[1] == 'deleted') continue; - $cookie .= $val . '; '; - } - $this->cookie = $cookie; - return true; - } else { - throw new Exception('登录失败,获取cookie失败'); - } - } elseif (strpos($response['body'], '验证码错误')) { - throw new Exception('登录失败,需输入验证码'); - } elseif (strpos($response['body'], '密码错误')) { - throw new Exception('登录失败,用户名或密码错误'); - } else { - throw new Exception('登录失败 (httpCode=' . $response['code'] . ')'); - } - } - - private function getMain() - { - $path = '/vhost/'; - http_request($this->url . $path, null, null, $this->cookie, null, $this->proxy); - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +url = rtrim($config['url'], '/'); + $this->auth = $config['auth']; + $this->username = $config['username']; + $this->password = $config['password']; + $this->skey = $config['skey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->username)) throw new Exception('必填参数不能为空'); + $this->login(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $this->login(); + $this->log('登录成功 cookie:' . $this->cookie); + $this->getMain(); + + if ($config['type'] == '1' && !empty($config['domains'])) { + $domains = explode("\n", $config['domains']); + $success = 0; + $errmsg = null; + foreach ($domains as $domain) { + $domain = trim($domain); + if (empty($domain)) continue; + try { + $this->deployDomain($domain, $fullchain, $privatekey); + $this->log("域名 {$domain} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("域名 {$domain} 证书部署失败:" . $errmsg); + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '要部署的域名不存在'); + } + } else { + $this->deployAccount($fullchain, $privatekey); + $this->log("账号级SSL证书部署成功"); + } + } + + private function deployDomain($domain, $fullchain, $privatekey) + { + $path = '/vhost/?c=ssl&a=domainSsl'; + $post = [ + 'domain' => $domain, + 'certificate' => $fullchain, + 'certificate_key' => $privatekey, + ]; + $response = http_request($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); + if (strpos($response['body'], '成功')) { + return true; + } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { + throw new Exception(htmlspecialchars($match[1])); + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception(htmlspecialchars($response['body'])); + } else { + throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); + } + } + + private function deployAccount($fullchain, $privatekey) + { + $path = '/vhost/?c=ssl&a=ssl'; + $post = [ + 'certificate' => $fullchain, + 'certificate_key' => $privatekey, + ]; + $response = http_request($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); + if (strpos($response['body'], '成功')) { + return true; + } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { + throw new Exception(htmlspecialchars($match[1])); + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception(htmlspecialchars($response['body'])); + } else { + throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); + } + } + + private function login() + { + if ($this->auth == '1') { + return $this->loginBySkey(); + } else { + return $this->loginByPwd(); + } + } + + private function loginBySkey() + { + $url = $this->url . '/vhost/index.php?c=sso&a=hello&url=' . urlencode($this->url . '/index.php?'); + $response = http_request($url, null, null, null, null, $this->proxy); + if ($response['code'] == 302 && !empty($response['redirect_url'])) { + $cookie = ''; + if (isset($response['headers']['Set-Cookie'])) { + foreach ($response['headers']['Set-Cookie'] as $val) { + $arr = explode('=', $val); + if ($arr[1] == '' || $arr[1] == 'deleted') continue; + $cookie .= $val . '; '; + } + $query = parse_url($response['redirect_url'], PHP_URL_QUERY); + parse_str($query, $params); + if (isset($params['r'])) { + $sess_key = $params['r']; + $this->loginBySkey2($cookie, $sess_key); + $this->cookie = $cookie; + return true; + } else { + throw new Exception('获取SSO凭据失败,sess_key获取失败'); + } + } else { + throw new Exception('获取SSO凭据失败,获取cookie失败'); + } + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception('获取SSO凭据失败 (' . htmlspecialchars($response['body']) . ')'); + } else { + throw new Exception('获取SSO凭据失败 (httpCode=' . $response['code'] . ')'); + } + } + + private function loginBySkey2($cookie, $sess_key) + { + $s = md5($sess_key . $this->username . $sess_key . $this->skey); + $url = $this->url . '/vhost/index.php?c=sso&a=login&name=' . $this->username . '&r=' . $sess_key . '&s=' . $s; + $response = http_request($url, null, null, $cookie, null, $this->proxy); + if ($response['code'] == 302) { + return true; + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception('SSO登录失败 (' . htmlspecialchars($response['body']) . ')'); + } else { + throw new Exception('SSO登录失败 (httpCode=' . $response['code'] . ')'); + } + } + + private function loginByPwd() + { + $referer = $this->url . '/vhost/index.php?c=session&a=loginForm'; + $url = $this->url . '/vhost/index.php?c=session&a=login'; + $post = [ + 'username' => $this->username, + 'passwd' => $this->password, + ]; + $response = http_request($url, http_build_query($post), $referer, null, null, $this->proxy); + if ($response['code'] == 302) { + $cookie = ''; + if (isset($response['headers']['Set-Cookie'])) { + foreach ($response['headers']['Set-Cookie'] as $val) { + $arr = explode('=', $val); + if ($arr[1] == '' || $arr[1] == 'deleted') continue; + $cookie .= $val . '; '; + } + $this->cookie = $cookie; + return true; + } else { + throw new Exception('登录失败,获取cookie失败'); + } + } elseif (strpos($response['body'], '验证码错误')) { + throw new Exception('登录失败,需输入验证码'); + } elseif (strpos($response['body'], '密码错误')) { + throw new Exception('登录失败,用户名或密码错误'); + } else { + throw new Exception('登录失败 (httpCode=' . $response['code'] . ')'); + } + } + + private function getMain() + { + $path = '/vhost/'; + http_request($this->url . $path, null, null, $this->cookie, null, $this->proxy); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/kangleadmin.php b/app/lib/deploy/kangleadmin.php index 6da0a57..e91e517 100644 --- a/app/lib/deploy/kangleadmin.php +++ b/app/lib/deploy/kangleadmin.php @@ -1,173 +1,173 @@ -url = rtrim($config['url'], '/'); - if (empty($config['path'])) $config['path'] = '/admin'; - $this->path = rtrim($config['path'], '/'); - $this->username = $config['username']; - $this->skey = $config['skey']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->username) || empty($this->skey)) throw new Exception('必填参数不能为空'); - $this->login(); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if (empty($config['name'])) throw new Exception('网站用户名不能为空'); - $this->login(); - $this->log('登录成功 cookie:' . $this->cookie); - $this->loginVhost($config['name']); - - if ($config['type'] == '1' && !empty($config['domains'])) { - $domains = explode("\n", $config['domains']); - $success = 0; - $errmsg = null; - foreach ($domains as $domain) { - $domain = trim($domain); - if (empty($domain)) continue; - try { - $this->deployDomain($domain, $fullchain, $privatekey); - $this->log("域名 {$domain} 证书部署成功"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("域名 {$domain} 证书部署失败:" . $errmsg); - } - } - if ($success == 0) { - throw new Exception($errmsg ? $errmsg : '要部署的域名不存在'); - } - } else { - $this->deployAccount($fullchain, $privatekey); - $this->log("账号级SSL证书部署成功"); - } - } - - private function deployDomain($domain, $fullchain, $privatekey) - { - $path = '/vhost/?c=ssl&a=domainSsl'; - $post = [ - 'domain' => $domain, - 'certificate' => $fullchain, - 'certificate_key' => $privatekey, - ]; - $response = http_request($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); - if (strpos($response['body'], '成功')) { - return true; - } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { - throw new Exception(htmlspecialchars($match[1])); - } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { - throw new Exception(htmlspecialchars($response['body'])); - } else { - throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); - } - } - - private function deployAccount($fullchain, $privatekey) - { - $path = '/vhost/?c=ssl&a=ssl'; - $post = [ - 'certificate' => $fullchain, - 'certificate_key' => $privatekey, - ]; - $response = http_request($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); - if (strpos($response['body'], '成功')) { - return true; - } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { - throw new Exception(htmlspecialchars($match[1])); - } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { - throw new Exception(htmlspecialchars($response['body'])); - } else { - throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); - } - } - - private function login() - { - $url = $this->url . $this->path . '/index.php?c=sso&a=hello&url=' . urlencode($this->url . $this->path . '/index.php?'); - $response = http_request($url, null, null, null, null, $this->proxy); - if ($response['code'] == 302 && !empty($response['redirect_url'])) { - $cookie = ''; - if (isset($response['headers']['Set-Cookie'])) { - foreach ($response['headers']['Set-Cookie'] as $val) { - $arr = explode('=', $val); - if ($arr[1] == '' || $arr[1] == 'deleted') continue; - $cookie .= $val . '; '; - } - $query = parse_url($response['redirect_url'], PHP_URL_QUERY); - parse_str($query, $params); - if (isset($params['r'])) { - $sess_key = $params['r']; - $this->login2($cookie, $sess_key); - $this->cookie = $cookie; - return true; - } else { - throw new Exception('获取SSO凭据失败,sess_key获取失败'); - } - } else { - throw new Exception('获取SSO凭据失败,获取cookie失败'); - } - } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { - throw new Exception('获取SSO凭据失败 (' . htmlspecialchars($response['body']) . ')'); - } else { - throw new Exception('获取SSO凭据失败 (httpCode=' . $response['code'] . ')'); - } - } - - private function login2($cookie, $sess_key) - { - $s = md5($sess_key . $this->username . $sess_key . $this->skey); - $url = $this->url . $this->path . '/index.php?c=sso&a=login&name=' . $this->username . '&r=' . $sess_key . '&s=' . $s; - $response = http_request($url, null, null, $cookie, null, $this->proxy); - if ($response['code'] == 302) { - return true; - } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { - throw new Exception('SSO登录失败 (' . htmlspecialchars($response['body']) . ')'); - } else { - throw new Exception('SSO登录失败 (httpCode=' . $response['code'] . ')'); - } - } - - private function loginVhost($name) - { - $url = $this->url . $this->path . '/index.php?c=vhost&a=impLogin&name=' . $name; - $response = http_request($url, null, null, $this->cookie, null, $this->proxy); - if ($response['code'] == 302) { - http_request($this->url . '/vhost/', null, null, $this->cookie, null, $this->proxy); - } else { - throw new Exception('用户面板登录失败 (httpCode=' . $response['code'] . ')'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +url = rtrim($config['url'], '/'); + if (empty($config['path'])) $config['path'] = '/admin'; + $this->path = rtrim($config['path'], '/'); + $this->username = $config['username']; + $this->skey = $config['skey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->username) || empty($this->skey)) throw new Exception('必填参数不能为空'); + $this->login(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['name'])) throw new Exception('网站用户名不能为空'); + $this->login(); + $this->log('登录成功 cookie:' . $this->cookie); + $this->loginVhost($config['name']); + + if ($config['type'] == '1' && !empty($config['domains'])) { + $domains = explode("\n", $config['domains']); + $success = 0; + $errmsg = null; + foreach ($domains as $domain) { + $domain = trim($domain); + if (empty($domain)) continue; + try { + $this->deployDomain($domain, $fullchain, $privatekey); + $this->log("域名 {$domain} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("域名 {$domain} 证书部署失败:" . $errmsg); + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '要部署的域名不存在'); + } + } else { + $this->deployAccount($fullchain, $privatekey); + $this->log("账号级SSL证书部署成功"); + } + } + + private function deployDomain($domain, $fullchain, $privatekey) + { + $path = '/vhost/?c=ssl&a=domainSsl'; + $post = [ + 'domain' => $domain, + 'certificate' => $fullchain, + 'certificate_key' => $privatekey, + ]; + $response = http_request($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); + if (strpos($response['body'], '成功')) { + return true; + } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { + throw new Exception(htmlspecialchars($match[1])); + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception(htmlspecialchars($response['body'])); + } else { + throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); + } + } + + private function deployAccount($fullchain, $privatekey) + { + $path = '/vhost/?c=ssl&a=ssl'; + $post = [ + 'certificate' => $fullchain, + 'certificate_key' => $privatekey, + ]; + $response = http_request($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); + if (strpos($response['body'], '成功')) { + return true; + } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { + throw new Exception(htmlspecialchars($match[1])); + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception(htmlspecialchars($response['body'])); + } else { + throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); + } + } + + private function login() + { + $url = $this->url . $this->path . '/index.php?c=sso&a=hello&url=' . urlencode($this->url . $this->path . '/index.php?'); + $response = http_request($url, null, null, null, null, $this->proxy); + if ($response['code'] == 302 && !empty($response['redirect_url'])) { + $cookie = ''; + if (isset($response['headers']['Set-Cookie'])) { + foreach ($response['headers']['Set-Cookie'] as $val) { + $arr = explode('=', $val); + if ($arr[1] == '' || $arr[1] == 'deleted') continue; + $cookie .= $val . '; '; + } + $query = parse_url($response['redirect_url'], PHP_URL_QUERY); + parse_str($query, $params); + if (isset($params['r'])) { + $sess_key = $params['r']; + $this->login2($cookie, $sess_key); + $this->cookie = $cookie; + return true; + } else { + throw new Exception('获取SSO凭据失败,sess_key获取失败'); + } + } else { + throw new Exception('获取SSO凭据失败,获取cookie失败'); + } + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception('获取SSO凭据失败 (' . htmlspecialchars($response['body']) . ')'); + } else { + throw new Exception('获取SSO凭据失败 (httpCode=' . $response['code'] . ')'); + } + } + + private function login2($cookie, $sess_key) + { + $s = md5($sess_key . $this->username . $sess_key . $this->skey); + $url = $this->url . $this->path . '/index.php?c=sso&a=login&name=' . $this->username . '&r=' . $sess_key . '&s=' . $s; + $response = http_request($url, null, null, $cookie, null, $this->proxy); + if ($response['code'] == 302) { + return true; + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception('SSO登录失败 (' . htmlspecialchars($response['body']) . ')'); + } else { + throw new Exception('SSO登录失败 (httpCode=' . $response['code'] . ')'); + } + } + + private function loginVhost($name) + { + $url = $this->url . $this->path . '/index.php?c=vhost&a=impLogin&name=' . $name; + $response = http_request($url, null, null, $this->cookie, null, $this->proxy); + if ($response['code'] == 302) { + http_request($this->url . '/vhost/', null, null, $this->cookie, null, $this->proxy); + } else { + throw new Exception('用户面板登录失败 (httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/ksyun.php b/app/lib/deploy/ksyun.php index 0a559cc..99bc621 100644 --- a/app/lib/deploy/ksyun.php +++ b/app/lib/deploy/ksyun.php @@ -1,79 +1,79 @@ -AccessKeyId = $config['AccessKeyId']; - $this->SecretAccessKey = $config['SecretAccessKey']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - } - - public function check() - { - if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); - $client = new KsyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cdn.api.ksyun.com', 'cdn', 'cn-shanghai-2', $this->proxy); - $client->request('GET', 'GetCertificates', '2016-09-01', '/2016-09-01/cert/GetCertificates'); - return true; - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $this->deploy_cdn($fullchain, $privatekey, $config, $info); - } - - public function deploy_cdn($fullchain, $privatekey, $config, &$info) - { - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - $domains = explode(',', $config['domain']); - - $client = new KsyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cdn.api.ksyun.com', 'cdn', 'cn-shanghai-2', $this->proxy); - $param = [ - 'PageSize' => 100, - 'PageNumber' => 1, - ]; - $domain_ids = []; - $result = $client->request('GET', 'GetCdnDomains', '2019-06-01', '/2019-06-01/domain/GetCdnDomains', $param); - foreach ($result['Domains'] as $row) { - if (in_array($row['DomainName'], $domains)) { - $domain_ids[] = $row['DomainId']; - } - } - if (count($domain_ids) == 0) throw new Exception('未找到对应的CDN域名'); - $param = [ - 'Enable' => 'on', - 'DomainIds' => implode(',', $domain_ids), - 'CertificateName' => $config['cert_name'], - 'ServerCertificate' => $fullchain, - 'PrivateKey' => $privatekey, - ]; - $result = $client->request('POST', 'ConfigCertificate', '2016-09-01', '/2016-09-01/cert/ConfigCertificate', $param); - $this->log('CDN证书部署成功,证书ID:' . $result['CertificateId']); - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $client = new KsyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cdn.api.ksyun.com', 'cdn', 'cn-shanghai-2', $this->proxy); + $client->request('GET', 'GetCertificates', '2016-09-01', '/2016-09-01/cert/GetCertificates'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $this->deploy_cdn($fullchain, $privatekey, $config, $info); + } + + public function deploy_cdn($fullchain, $privatekey, $config, &$info) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + $domains = explode(',', $config['domain']); + + $client = new KsyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cdn.api.ksyun.com', 'cdn', 'cn-shanghai-2', $this->proxy); + $param = [ + 'PageSize' => 100, + 'PageNumber' => 1, + ]; + $domain_ids = []; + $result = $client->request('GET', 'GetCdnDomains', '2019-06-01', '/2019-06-01/domain/GetCdnDomains', $param); + foreach ($result['Domains'] as $row) { + if (in_array($row['DomainName'], $domains)) { + $domain_ids[] = $row['DomainId']; + } + } + if (count($domain_ids) == 0) throw new Exception('未找到对应的CDN域名'); + $param = [ + 'Enable' => 'on', + 'DomainIds' => implode(',', $domain_ids), + 'CertificateName' => $config['cert_name'], + 'ServerCertificate' => $fullchain, + 'PrivateKey' => $privatekey, + ]; + $result = $client->request('POST', 'ConfigCertificate', '2016-09-01', '/2016-09-01/cert/ConfigCertificate', $param); + $this->log('CDN证书部署成功,证书ID:' . $result['CertificateId']); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/lecdn.php b/app/lib/deploy/lecdn.php index 6ec628a..3a5b143 100644 --- a/app/lib/deploy/lecdn.php +++ b/app/lib/deploy/lecdn.php @@ -1,122 +1,122 @@ -url = rtrim($config['url'], '/'); - $this->email = $config['email']; - $this->password = $config['password']; - $this->auth = isset($config['auth']) ? intval($config['auth']) : 0; - if ($this->auth == 1) { - $this->apiKey = $config['api_key']; - } - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if ($this->auth == 1) { - if (empty($this->url) || empty($this->apiKey)) throw new Exception('API访问令牌不能为空'); - $this->request('/prod-api/system/info'); - } else { - if (empty($this->url) || empty($this->email) || empty($this->password)) throw new Exception('账号和密码不能为空'); - $this->login(); - } - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $id = $config['id']; - if (empty($id)) throw new Exception('证书ID不能为空'); - - if ($this->auth == 0) { - $this->login(); - } - - try { - $data = $this->request('/prod-api/certificate/' . $id); - } catch (Exception $e) { - throw new Exception('证书ID:' . $id . '获取失败:' . $e->getMessage()); - } - - $params = [ - 'id' => intval($id), - 'name' => $data['name'], - 'description' => $data['description'], - 'type' => 'upload', - 'ssl_pem' => base64_encode($fullchain), - 'ssl_key' => base64_encode($privatekey), - 'auto_renewal' => false, - ]; - $this->request('/prod-api/certificate/' . $id, $params, 'PUT'); - $this->log("证书ID:{$id}更新成功!"); - } - - private function login() - { - $path = '/prod-api/login'; - $params = [ - 'email' => $this->email, - 'username' => $this->email, - 'password' => $this->password, - ]; - $result = $this->request($path, $params); - if (isset($result['token'])) { - $this->accessToken = $result['token']; - } else { - throw new Exception('登录成功,获取access_token失败'); - } - } - - private function request($path, $params = null, $method = null) - { - $url = $this->url . $path; - $headers = []; - $body = null; - if ($this->accessToken) { - $headers['Authorization'] = 'Bearer ' . $this->accessToken; - } elseif ($this->auth == 1 && $this->apiKey) { - $headers['Authorization'] = $this->apiKey; - } - if ($params) { - $headers['Content-Type'] = 'application/json;charset=UTF-8'; - $body = json_encode($params); - } - $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['data'] ?? null; - } elseif (isset($result['message'])) { - throw new Exception($result['message']); - } else { - throw new Exception('返回数据解析失败'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +url = rtrim($config['url'], '/'); + $this->email = $config['email']; + $this->password = $config['password']; + $this->auth = isset($config['auth']) ? intval($config['auth']) : 0; + if ($this->auth == 1) { + $this->apiKey = $config['api_key']; + } + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if ($this->auth == 1) { + if (empty($this->url) || empty($this->apiKey)) throw new Exception('API访问令牌不能为空'); + $this->request('/prod-api/system/info'); + } else { + if (empty($this->url) || empty($this->email) || empty($this->password)) throw new Exception('账号和密码不能为空'); + $this->login(); + } + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $id = $config['id']; + if (empty($id)) throw new Exception('证书ID不能为空'); + + if ($this->auth == 0) { + $this->login(); + } + + try { + $data = $this->request('/prod-api/certificate/' . $id); + } catch (Exception $e) { + throw new Exception('证书ID:' . $id . '获取失败:' . $e->getMessage()); + } + + $params = [ + 'id' => intval($id), + 'name' => $data['name'], + 'description' => $data['description'], + 'type' => 'upload', + 'ssl_pem' => base64_encode($fullchain), + 'ssl_key' => base64_encode($privatekey), + 'auto_renewal' => false, + ]; + $this->request('/prod-api/certificate/' . $id, $params, 'PUT'); + $this->log("证书ID:{$id}更新成功!"); + } + + private function login() + { + $path = '/prod-api/login'; + $params = [ + 'email' => $this->email, + 'username' => $this->email, + 'password' => $this->password, + ]; + $result = $this->request($path, $params); + if (isset($result['token'])) { + $this->accessToken = $result['token']; + } else { + throw new Exception('登录成功,获取access_token失败'); + } + } + + private function request($path, $params = null, $method = null) + { + $url = $this->url . $path; + $headers = []; + $body = null; + if ($this->accessToken) { + $headers['Authorization'] = 'Bearer ' . $this->accessToken; + } elseif ($this->auth == 1 && $this->apiKey) { + $headers['Authorization'] = $this->apiKey; + } + if ($params) { + $headers['Content-Type'] = 'application/json;charset=UTF-8'; + $body = json_encode($params); + } + $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['data'] ?? null; + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + throw new Exception('返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/local.php b/app/lib/deploy/local.php index d51458c..6245926 100644 --- a/app/lib/deploy/local.php +++ b/app/lib/deploy/local.php @@ -1,77 +1,77 @@ -log('证书已保存到:' . $config['pem_cert_file']); - } else { - throw new Exception('证书保存到' . $config['pem_cert_file'] . '失败,请检查目录权限'); - } - if (file_put_contents($config['pem_key_file'], $privatekey)) { - $this->log('私钥已保存到:' . $config['pem_key_file']); - } else { - throw new Exception('私钥保存到' . $config['pem_key_file'] . '失败,请检查目录权限'); - } - } elseif ($config['format'] == 'pfx') { - $dir = dirname($config['pfx_file']); - if (!is_dir($dir)) throw new Exception($dir . ' 目录不存在'); - if (!is_writable($dir)) throw new Exception($dir . ' 目录不可写'); - - $pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null); - if (file_put_contents($config['pfx_file'], $pfx)) { - $this->log('PFX证书已保存到:' . $config['pfx_file']); - } else { - throw new Exception('PFX证书保存到' . $config['pfx_file'] . '失败,请检查目录权限'); - } - } - if (!empty($config['cmd'])) { - $cmds = explode("\n", $config['cmd']); - foreach ($cmds as $cmd) { - $cmd = trim($cmd); - if (empty($cmd)) continue; - $this->log('执行命令:' . $cmd); - $output = []; - $ret = 0; - exec($cmd, $output, $ret); - if ($ret == 0) { - $this->log('执行命令成功:' . implode("\n", $output)); - } else { - throw new Exception('执行命令失败:' . implode("\n", $output)); - } - } - } - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - public function setLogger($logger) - { - $this->logger = $logger; - } -} +log('证书已保存到:' . $config['pem_cert_file']); + } else { + throw new Exception('证书保存到' . $config['pem_cert_file'] . '失败,请检查目录权限'); + } + if (file_put_contents($config['pem_key_file'], $privatekey)) { + $this->log('私钥已保存到:' . $config['pem_key_file']); + } else { + throw new Exception('私钥保存到' . $config['pem_key_file'] . '失败,请检查目录权限'); + } + } elseif ($config['format'] == 'pfx') { + $dir = dirname($config['pfx_file']); + if (!is_dir($dir)) throw new Exception($dir . ' 目录不存在'); + if (!is_writable($dir)) throw new Exception($dir . ' 目录不可写'); + + $pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null); + if (file_put_contents($config['pfx_file'], $pfx)) { + $this->log('PFX证书已保存到:' . $config['pfx_file']); + } else { + throw new Exception('PFX证书保存到' . $config['pfx_file'] . '失败,请检查目录权限'); + } + } + if (!empty($config['cmd'])) { + $cmds = explode("\n", $config['cmd']); + foreach ($cmds as $cmd) { + $cmd = trim($cmd); + if (empty($cmd)) continue; + $this->log('执行命令:' . $cmd); + $output = []; + $ret = 0; + exec($cmd, $output, $ret); + if ($ret == 0) { + $this->log('执行命令成功:' . implode("\n", $output)); + } else { + throw new Exception('执行命令失败:' . implode("\n", $output)); + } + } + } + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + public function setLogger($logger) + { + $this->logger = $logger; + } +} diff --git a/app/lib/deploy/lucky.php b/app/lib/deploy/lucky.php index 94f4bd4..f9da48f 100644 --- a/app/lib/deploy/lucky.php +++ b/app/lib/deploy/lucky.php @@ -1,114 +1,114 @@ -url = rtrim($config['url'], '/') . (!empty($config['path']) ? $config['path'] : ''); - $this->opentoken = $config['opentoken']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->opentoken)) throw new Exception('请填写面板地址和OpenToken'); - $this->request("/api/modules/list"); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $domains = $config['domainList']; - if (empty($domains)) throw new Exception('没有设置要部署的域名'); - - try { - $data = $this->request("/api/ssl"); - $this->log('获取证书列表成功'); - } catch (Exception $e) { - throw new Exception('获取证书列表失败:' . $e->getMessage()); - } - - $success = 0; - $errmsg = null; - if (!empty($data['list'])) { - foreach ($data['list'] as $row) { - if (empty($row['CertsInfo']['Domains'])) continue; - $cert_domains = $row['CertsInfo']['Domains']; - $flag = false; - foreach ($cert_domains as $domain) { - if (in_array($domain, $domains)) { - $flag = true; - break; - } - } - if ($flag) { - $params = [ - 'Key' => $row['Key'], - 'CertBase64' => base64_encode($fullchain), - 'KeyBase64' => base64_encode($privatekey), - 'AddFrom' => 'file', - 'Enable' => true, - 'MappingToPath' => false, - 'Remark' => $row['Remark'] ?: '', - 'AllSyncClient' => false, - ]; - try { - $this->request('/api/ssl', $params, 'PUT'); - $this->log("证书ID:{$row['Key']}更新成功!"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("证书ID:{$row['Key']}更新失败:" . $errmsg); - } - } - } - } - if ($success == 0) { - throw new Exception($errmsg ? $errmsg : '没有要更新的证书'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - private function request($path, $params = null, $method = null) - { - $url = $this->url . $path; - - $headers = [ - 'openToken' => $this->opentoken, - ]; - $body = null; - if ($params) { - $body = json_encode($params); - $headers['Content-Type'] = 'application/json'; - } - $response = http_request($url, $body, null, null, $headers, $this->proxy, $method); - $result = json_decode($response['body'], true); - if (isset($result['ret']) && $result['ret'] == 0) { - return $result; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); - } - } -} +url = rtrim($config['url'], '/') . (!empty($config['path']) ? $config['path'] : ''); + $this->opentoken = $config['opentoken']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->opentoken)) throw new Exception('请填写面板地址和OpenToken'); + $this->request("/api/modules/list"); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domains = $config['domainList']; + if (empty($domains)) throw new Exception('没有设置要部署的域名'); + + try { + $data = $this->request("/api/ssl"); + $this->log('获取证书列表成功'); + } catch (Exception $e) { + throw new Exception('获取证书列表失败:' . $e->getMessage()); + } + + $success = 0; + $errmsg = null; + if (!empty($data['list'])) { + foreach ($data['list'] as $row) { + if (empty($row['CertsInfo']['Domains'])) continue; + $cert_domains = $row['CertsInfo']['Domains']; + $flag = false; + foreach ($cert_domains as $domain) { + if (in_array($domain, $domains)) { + $flag = true; + break; + } + } + if ($flag) { + $params = [ + 'Key' => $row['Key'], + 'CertBase64' => base64_encode($fullchain), + 'KeyBase64' => base64_encode($privatekey), + 'AddFrom' => 'file', + 'Enable' => true, + 'MappingToPath' => false, + 'Remark' => $row['Remark'] ?: '', + 'AllSyncClient' => false, + ]; + try { + $this->request('/api/ssl', $params, 'PUT'); + $this->log("证书ID:{$row['Key']}更新成功!"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("证书ID:{$row['Key']}更新失败:" . $errmsg); + } + } + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '没有要更新的证书'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($path, $params = null, $method = null) + { + $url = $this->url . $path; + + $headers = [ + 'openToken' => $this->opentoken, + ]; + $body = null; + if ($params) { + $body = json_encode($params); + $headers['Content-Type'] = 'application/json'; + } + $response = http_request($url, $body, null, null, $headers, $this->proxy, $method); + $result = json_decode($response['body'], true); + if (isset($result['ret']) && $result['ret'] == 0) { + return $result; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } +} diff --git a/app/lib/deploy/mwpanel.php b/app/lib/deploy/mwpanel.php index ed79c89..15bef97 100644 --- a/app/lib/deploy/mwpanel.php +++ b/app/lib/deploy/mwpanel.php @@ -1,127 +1,127 @@ -url = rtrim($config['url'], '/'); - $this->appid = $config['appid']; - $this->appsecret = $config['appsecret']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->appid) || empty($this->appsecret)) throw new Exception('请填写面板地址和接口密钥'); - - $path = '/task/count'; - $response = $this->request($path); - $result = json_decode($response, true); - if (isset($result['status']) && $result['status'] == true) { - return true; - } else { - throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接'); - } - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if ($config['type'] == '1') { - $this->deployPanel($fullchain, $privatekey); - $this->log("面板证书部署成功"); - return; - } - $sites = explode("\n", $config['sites']); - $success = 0; - $errmsg = null; - foreach ($sites as $site) { - $siteName = trim($site); - if (empty($siteName)) continue; - try { - $this->deploySite($siteName, $fullchain, $privatekey); - $this->log("网站 {$siteName} 证书部署成功"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("网站 {$siteName} 证书部署失败:" . $errmsg); - } - } - if ($success == 0) { - throw new Exception($errmsg ? $errmsg : '要部署的网站不存在'); - } - } - - private function deployPanel($fullchain, $privatekey) - { - $path = '/setting/save_panel_ssl'; - $data = [ - 'privateKey' => $privatekey, - 'certPem' => $fullchain, - 'choose' => 'local', - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['status']) && $result['status']) { - return true; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } - - private function deploySite($siteName, $fullchain, $privatekey) - { - $path = '/site/set_ssl'; - $data = [ - 'type' => '1', - 'siteName' => $siteName, - 'key' => $privatekey, - 'csr' => $fullchain, - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['status']) && $result['status']) { - return true; - } elseif (isset($result['msg'])) { - throw new Exception($result['msg']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - private function request($path, $params = null) - { - $url = $this->url . $path; - - $headers = [ - 'app-id' => $this->appid, - 'app-secret' => $this->appsecret, - ]; - $response = http_request($url, $params ? http_build_query($params) : null, null, null, $headers, $this->proxy); - return $response['body']; - } -} +url = rtrim($config['url'], '/'); + $this->appid = $config['appid']; + $this->appsecret = $config['appsecret']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->appid) || empty($this->appsecret)) throw new Exception('请填写面板地址和接口密钥'); + + $path = '/task/count'; + $response = $this->request($path); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status'] == true) { + return true; + } else { + throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接'); + } + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if ($config['type'] == '1') { + $this->deployPanel($fullchain, $privatekey); + $this->log("面板证书部署成功"); + return; + } + $sites = explode("\n", $config['sites']); + $success = 0; + $errmsg = null; + foreach ($sites as $site) { + $siteName = trim($site); + if (empty($siteName)) continue; + try { + $this->deploySite($siteName, $fullchain, $privatekey); + $this->log("网站 {$siteName} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("网站 {$siteName} 证书部署失败:" . $errmsg); + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '要部署的网站不存在'); + } + } + + private function deployPanel($fullchain, $privatekey) + { + $path = '/setting/save_panel_ssl'; + $data = [ + 'privateKey' => $privatekey, + 'certPem' => $fullchain, + 'choose' => 'local', + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + + private function deploySite($siteName, $fullchain, $privatekey) + { + $path = '/site/set_ssl'; + $data = [ + 'type' => '1', + 'siteName' => $siteName, + 'key' => $privatekey, + 'csr' => $fullchain, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($path, $params = null) + { + $url = $this->url . $path; + + $headers = [ + 'app-id' => $this->appid, + 'app-secret' => $this->appsecret, + ]; + $response = http_request($url, $params ? http_build_query($params) : null, null, null, $headers, $this->proxy); + return $response['body']; + } +} diff --git a/app/lib/deploy/opanel.php b/app/lib/deploy/opanel.php index 984da5b..487daab 100644 --- a/app/lib/deploy/opanel.php +++ b/app/lib/deploy/opanel.php @@ -1,114 +1,114 @@ -url = rtrim($config['url'], '/') . '/api/' . (isset($config['version']) ? $config['version'] : 'v1'); - $this->key = $config['key']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥'); - $this->request("/settings/search"); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $domains = $config['domainList']; - if (empty($domains)) throw new Exception('没有设置要部署的域名'); - - $params = ['page' => 1, 'pageSize' => 500]; - try { - $data = $this->request("/websites/ssl/search", $params); - $this->log('获取证书列表成功(total=' . $data['total'] . ')'); - } catch (Exception $e) { - throw new Exception('获取证书列表失败:' . $e->getMessage()); - } - - $success = 0; - $errmsg = null; - if (!empty($data['items'])) { - foreach ($data['items'] as $row) { - if (empty($row['primaryDomain'])) continue; - $cert_domains = []; - $cert_domains[] = $row['primaryDomain']; - if (!empty($row['domains'])) $cert_domains += explode(',', $row['domains']); - $flag = false; - foreach ($cert_domains as $domain) { - if (in_array($domain, $domains) || in_array('*' . substr($domain, strpos($domain, '.')), $domains)) { - $flag = true; - break; - } - } - if ($flag) { - $params = [ - 'sslID' => $row['id'], - 'type' => 'paste', - 'certificate' => $fullchain, - 'privateKey' => $privatekey, - 'description' => '', - ]; - try { - $this->request('/websites/ssl/upload', $params); - $this->log("证书ID:{$row['id']}更新成功!"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("证书ID:{$row['id']}更新失败:" . $errmsg); - } - } - } - } - if ($success == 0) { - throw new Exception($errmsg ? $errmsg : '没有要更新的证书'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - private function request($path, $params = null) - { - $url = $this->url . $path; - - $timestamp = time() . ''; - $token = md5('1panel' . $this->key . $timestamp); - $headers = [ - '1Panel-Token' => $token, - '1Panel-Timestamp' => $timestamp - ]; - $body = $params ? json_encode($params) : '{}'; - if ($body) $headers['Content-Type'] = 'application/json'; - $response = http_request($url, $body, null, null, $headers, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['code']) && $result['code'] == 200) { - return $result['data'] ?? null; - } elseif (isset($result['message'])) { - throw new Exception($result['message']); - } else { - throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); - } - } -} +url = rtrim($config['url'], '/') . '/api/' . (isset($config['version']) ? $config['version'] : 'v1'); + $this->key = $config['key']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥'); + $this->request("/settings/search"); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domains = $config['domainList']; + if (empty($domains)) throw new Exception('没有设置要部署的域名'); + + $params = ['page' => 1, 'pageSize' => 500]; + try { + $data = $this->request("/websites/ssl/search", $params); + $this->log('获取证书列表成功(total=' . $data['total'] . ')'); + } catch (Exception $e) { + throw new Exception('获取证书列表失败:' . $e->getMessage()); + } + + $success = 0; + $errmsg = null; + if (!empty($data['items'])) { + foreach ($data['items'] as $row) { + if (empty($row['primaryDomain'])) continue; + $cert_domains = []; + $cert_domains[] = $row['primaryDomain']; + if (!empty($row['domains'])) $cert_domains += explode(',', $row['domains']); + $flag = false; + foreach ($cert_domains as $domain) { + if (in_array($domain, $domains) || in_array('*' . substr($domain, strpos($domain, '.')), $domains)) { + $flag = true; + break; + } + } + if ($flag) { + $params = [ + 'sslID' => $row['id'], + 'type' => 'paste', + 'certificate' => $fullchain, + 'privateKey' => $privatekey, + 'description' => '', + ]; + try { + $this->request('/websites/ssl/upload', $params); + $this->log("证书ID:{$row['id']}更新成功!"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("证书ID:{$row['id']}更新失败:" . $errmsg); + } + } + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '没有要更新的证书'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($path, $params = null) + { + $url = $this->url . $path; + + $timestamp = time() . ''; + $token = md5('1panel' . $this->key . $timestamp); + $headers = [ + '1Panel-Token' => $token, + '1Panel-Timestamp' => $timestamp + ]; + $body = $params ? json_encode($params) : '{}'; + if ($body) $headers['Content-Type'] = 'application/json'; + $response = http_request($url, $body, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 200) { + return $result['data'] ?? null; + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } +} diff --git a/app/lib/deploy/proxmox.php b/app/lib/deploy/proxmox.php index 80ae5cc..821d908 100644 --- a/app/lib/deploy/proxmox.php +++ b/app/lib/deploy/proxmox.php @@ -1,93 +1,93 @@ -url = rtrim($config['url'], '/'); - $this->api_user = $config['api_user']; - $this->api_key = $config['api_key']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->api_user) || empty($this->api_key)) throw new Exception('必填内容不能为空'); - - $path = '/api2/json/access'; - $this->send_request($path); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if (empty($config['node'])) throw new Exception('节点名称不能为空'); - $cert_hash = openssl_x509_fingerprint($fullchain, 'sha256'); - if (!$cert_hash) throw new Exception('证书解析失败'); - - $path = '/api2/json/nodes/' . $config['node'] . '/certificates/info'; - $list = $this->send_request($path); - foreach ($list as $item) { - $fingerprint = strtolower(str_replace(':', '', $item['fingerprint'])); - if ($fingerprint == $cert_hash) { - $this->log('节点:' . $config['node'] . ' 证书已存在'); - return; - } - } - - $path = '/api2/json/nodes/' . $config['node'] . '/certificates/custom'; - $params = [ - 'certificates' => $fullchain, - 'key' => $privatekey, - 'force' => 1, - 'restart' => 1, - ]; - $this->send_request($path, $params); - $this->log('节点:' . $config['node'] . ' 证书部署成功!'); - } - - private function send_request($path, $params = null) - { - $url = $this->url . $path; - $headers = ['Authorization' => 'PVEAPIToken=' . $this->api_user . '=' . $this->api_key]; - $post = $params ? http_build_query($params) : null; - $response = http_request($url, $post, null, null, $headers, $this->proxy); - if ($response['code'] == 200) { - $result = json_decode($response['body'], true); - if (isset($result['data'])) { - return $result['data']; - } elseif (isset($result['errors'])) { - if (is_array($result['errors'])) { - $result['errors'] = implode(';', $result['errors']); - } - throw new Exception($result['errors']); - } else { - throw new Exception('返回数据解析失败'); - } - } else { - throw new Exception('请求失败(httpCode=' . $response['code'] . ', body=' . $response['body'] . ')'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +url = rtrim($config['url'], '/'); + $this->api_user = $config['api_user']; + $this->api_key = $config['api_key']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->api_user) || empty($this->api_key)) throw new Exception('必填内容不能为空'); + + $path = '/api2/json/access'; + $this->send_request($path); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['node'])) throw new Exception('节点名称不能为空'); + $cert_hash = openssl_x509_fingerprint($fullchain, 'sha256'); + if (!$cert_hash) throw new Exception('证书解析失败'); + + $path = '/api2/json/nodes/' . $config['node'] . '/certificates/info'; + $list = $this->send_request($path); + foreach ($list as $item) { + $fingerprint = strtolower(str_replace(':', '', $item['fingerprint'])); + if ($fingerprint == $cert_hash) { + $this->log('节点:' . $config['node'] . ' 证书已存在'); + return; + } + } + + $path = '/api2/json/nodes/' . $config['node'] . '/certificates/custom'; + $params = [ + 'certificates' => $fullchain, + 'key' => $privatekey, + 'force' => 1, + 'restart' => 1, + ]; + $this->send_request($path, $params); + $this->log('节点:' . $config['node'] . ' 证书部署成功!'); + } + + private function send_request($path, $params = null) + { + $url = $this->url . $path; + $headers = ['Authorization' => 'PVEAPIToken=' . $this->api_user . '=' . $this->api_key]; + $post = $params ? http_build_query($params) : null; + $response = http_request($url, $post, null, null, $headers, $this->proxy); + if ($response['code'] == 200) { + $result = json_decode($response['body'], true); + if (isset($result['data'])) { + return $result['data']; + } elseif (isset($result['errors'])) { + if (is_array($result['errors'])) { + $result['errors'] = implode(';', $result['errors']); + } + throw new Exception($result['errors']); + } else { + throw new Exception('返回数据解析失败'); + } + } else { + throw new Exception('请求失败(httpCode=' . $response['code'] . ', body=' . $response['body'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/qiniu.php b/app/lib/deploy/qiniu.php index 79af719..b023e0a 100644 --- a/app/lib/deploy/qiniu.php +++ b/app/lib/deploy/qiniu.php @@ -1,159 +1,159 @@ -AccessKey = $config['AccessKey']; - $this->SecretKey = $config['SecretKey']; - $this->client = new QiniuClient($this->AccessKey, $this->SecretKey, isset($config['proxy']) ? $config['proxy'] == 1 : false); - } - - public function check() - { - if (empty($this->AccessKey) || empty($this->SecretKey)) throw new Exception('必填参数不能为空'); - $this->client->request('GET', '/sslcert'); - return true; - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $domains = $config['domain']; - if (empty($domains)) throw new Exception('绑定的域名不能为空'); - - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $cert_id = $this->get_cert_id($fullchain, $privatekey, $certInfo['subject']['CN'], $cert_name); - - foreach (explode(',', $domains) as $domain) { - if (empty($domain)) continue; - if ($config['product'] == 'cdn') { - $this->deploy_cdn($domain, $cert_id); - } elseif ($config['product'] == 'oss') { - $this->deploy_oss($domain, $cert_id); - } elseif ($config['product'] == 'pili') { - $this->deploy_pili($config['pili_hub'], $domain, $cert_name); - } else { - throw new Exception('未知的产品类型'); - } - } - $info['cert_id'] = $cert_id; - $info['cert_name'] = $cert_name; - } - - private function deploy_cdn($domain, $cert_id) - { - try { - $data = $this->client->request('GET', '/domain/' . $domain); - } catch (Exception $e) { - throw new Exception('获取域名信息失败:' . $e->getMessage()); - } - if (isset($data['https']['certId']) && $data['https']['certId'] == $cert_id) { - $this->log('域名 ' . $domain . ' 证书已部署,无需重复操作'); - return; - } - - if (empty($data['https']['certId'])) { - $param = [ - 'certid' => $cert_id, - ]; - $this->client->request('PUT', '/domain/' . $domain . '/sslize', null, $param); - } else { - $param = [ - 'certid' => $cert_id, - 'forceHttps' => $data['https']['forceHttps'], - 'http2Enable' => $data['https']['http2Enable'], - ]; - $this->client->request('PUT', '/domain/' . $domain . '/httpsconf', null, $param); - } - $this->log('CDN域名 ' . $domain . ' 证书部署成功!'); - } - - private function deploy_oss($domain, $cert_id) - { - $param = [ - 'certid' => $cert_id, - 'domain' => $domain, - ]; - $this->client->request('POST', '/cert/bind', null, $param); - $this->log('OSS域名 ' . $domain . ' 证书部署成功!'); - } - - private function deploy_pili($hub, $domain, $cert_name) - { - $param = [ - 'CertName' => $cert_name, - ]; - $this->client->pili_request('POST', '/v2/hubs/'.$hub.'/domains/'.$domain.'/cert', null, $param); - $this->log('视频直播域名 ' . $domain . ' 证书部署成功!'); - } - - private function get_cert_id($fullchain, $privatekey, $common_name, $cert_name) - { - $cert_id = null; - $marker = ''; - do { - $query = ['marker' => $marker, 'limit' => 100]; - $data = $this->client->request('GET', '/sslcert', $query); - if (empty($data['certs'])) break; - $marker = $data['marker']; - foreach ($data['certs'] as $cert) { - if ($cert_name == $cert['name']) { - $cert_id = $cert['certid']; - $this->log('证书' . $cert_name . '已存在,证书ID:' . $cert_id); - } elseif ($cert['not_after'] < time()) { - try { - $this->client->request('DELETE', '/sslcert/' . $cert['certid']); - $this->log('证书' . $cert['name'] . '已过期,删除证书成功'); - } catch (Exception $e) { - $this->log('证书' . $cert['name'] . '已过期,删除证书失败:' . $e->getMessage()); - } - usleep(300000); - } - } - } while ($marker != ''); - - if (!$cert_id) { - $param = [ - 'name' => $cert_name, - 'common_name' => $common_name, - 'pri' => $privatekey, - 'ca' => $fullchain, - ]; - try { - $data = $this->client->request('POST', '/sslcert', null, $param); - } catch (Exception $e) { - throw new Exception('上传证书失败:' . $e->getMessage()); - } - $this->log('上传证书成功,证书ID:' . $data['certID']); - $cert_id = $data['certID']; - usleep(500000); - } - return $cert_id; - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +AccessKey = $config['AccessKey']; + $this->SecretKey = $config['SecretKey']; + $this->client = new QiniuClient($this->AccessKey, $this->SecretKey, isset($config['proxy']) ? $config['proxy'] == 1 : false); + } + + public function check() + { + if (empty($this->AccessKey) || empty($this->SecretKey)) throw new Exception('必填参数不能为空'); + $this->client->request('GET', '/sslcert'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domains = $config['domain']; + if (empty($domains)) throw new Exception('绑定的域名不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $cert_id = $this->get_cert_id($fullchain, $privatekey, $certInfo['subject']['CN'], $cert_name); + + foreach (explode(',', $domains) as $domain) { + if (empty($domain)) continue; + if ($config['product'] == 'cdn') { + $this->deploy_cdn($domain, $cert_id); + } elseif ($config['product'] == 'oss') { + $this->deploy_oss($domain, $cert_id); + } elseif ($config['product'] == 'pili') { + $this->deploy_pili($config['pili_hub'], $domain, $cert_name); + } else { + throw new Exception('未知的产品类型'); + } + } + $info['cert_id'] = $cert_id; + $info['cert_name'] = $cert_name; + } + + private function deploy_cdn($domain, $cert_id) + { + try { + $data = $this->client->request('GET', '/domain/' . $domain); + } catch (Exception $e) { + throw new Exception('获取域名信息失败:' . $e->getMessage()); + } + if (isset($data['https']['certId']) && $data['https']['certId'] == $cert_id) { + $this->log('域名 ' . $domain . ' 证书已部署,无需重复操作'); + return; + } + + if (empty($data['https']['certId'])) { + $param = [ + 'certid' => $cert_id, + ]; + $this->client->request('PUT', '/domain/' . $domain . '/sslize', null, $param); + } else { + $param = [ + 'certid' => $cert_id, + 'forceHttps' => $data['https']['forceHttps'], + 'http2Enable' => $data['https']['http2Enable'], + ]; + $this->client->request('PUT', '/domain/' . $domain . '/httpsconf', null, $param); + } + $this->log('CDN域名 ' . $domain . ' 证书部署成功!'); + } + + private function deploy_oss($domain, $cert_id) + { + $param = [ + 'certid' => $cert_id, + 'domain' => $domain, + ]; + $this->client->request('POST', '/cert/bind', null, $param); + $this->log('OSS域名 ' . $domain . ' 证书部署成功!'); + } + + private function deploy_pili($hub, $domain, $cert_name) + { + $param = [ + 'CertName' => $cert_name, + ]; + $this->client->pili_request('POST', '/v2/hubs/'.$hub.'/domains/'.$domain.'/cert', null, $param); + $this->log('视频直播域名 ' . $domain . ' 证书部署成功!'); + } + + private function get_cert_id($fullchain, $privatekey, $common_name, $cert_name) + { + $cert_id = null; + $marker = ''; + do { + $query = ['marker' => $marker, 'limit' => 100]; + $data = $this->client->request('GET', '/sslcert', $query); + if (empty($data['certs'])) break; + $marker = $data['marker']; + foreach ($data['certs'] as $cert) { + if ($cert_name == $cert['name']) { + $cert_id = $cert['certid']; + $this->log('证书' . $cert_name . '已存在,证书ID:' . $cert_id); + } elseif ($cert['not_after'] < time()) { + try { + $this->client->request('DELETE', '/sslcert/' . $cert['certid']); + $this->log('证书' . $cert['name'] . '已过期,删除证书成功'); + } catch (Exception $e) { + $this->log('证书' . $cert['name'] . '已过期,删除证书失败:' . $e->getMessage()); + } + usleep(300000); + } + } + } while ($marker != ''); + + if (!$cert_id) { + $param = [ + 'name' => $cert_name, + 'common_name' => $common_name, + 'pri' => $privatekey, + 'ca' => $fullchain, + ]; + try { + $data = $this->client->request('POST', '/sslcert', null, $param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $this->log('上传证书成功,证书ID:' . $data['certID']); + $cert_id = $data['certID']; + usleep(500000); + } + return $cert_id; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/rainyun.php b/app/lib/deploy/rainyun.php index c5e3f40..f204b21 100644 --- a/app/lib/deploy/rainyun.php +++ b/app/lib/deploy/rainyun.php @@ -1,78 +1,78 @@ -apikey = $config['apikey']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->apikey)) throw new Exception('ApiKey不能为空'); - $this->request('/product/'); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if (empty($config['id'])) throw new Exception('证书ID不能为空'); - - $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'] . '更新成功!'); - } - - private function request($path, $params = null, $method = null) - { - $url = $this->url . $path; - $headers = [ - 'x-api-key' => $this->apikey, - ]; - $body = null; - if ($params) { - $headers['Content-Type'] = 'application/json'; - $body = json_encode($params); - } - $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; - } elseif (isset($result['message'])) { - throw new Exception($result['message']); - } else { - if (!empty($response['body'])) $this->log('Response:' . $response['body']); - throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +apikey = $config['apikey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->apikey)) throw new Exception('ApiKey不能为空'); + $this->request('/product/'); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['id'])) throw new Exception('证书ID不能为空'); + + $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'] . '更新成功!'); + } + + private function request($path, $params = null, $method = null) + { + $url = $this->url . $path; + $headers = [ + 'x-api-key' => $this->apikey, + ]; + $body = null; + if ($params) { + $headers['Content-Type'] = 'application/json'; + $body = json_encode($params); + } + $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; + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + if (!empty($response['body'])) $this->log('Response:' . $response['body']); + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/safeline.php b/app/lib/deploy/safeline.php index 91aa45c..e7b556e 100644 --- a/app/lib/deploy/safeline.php +++ b/app/lib/deploy/safeline.php @@ -1,112 +1,112 @@ -url = rtrim($config['url'], '/'); - $this->token = $config['token']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->token)) throw new Exception('请填写控制台地址和API Token'); - $this->request('/api/open/system'); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $domains = $config['domainList']; - if (empty($domains)) throw new Exception('没有设置要部署的域名'); - - try { - $data = $this->request('/api/open/cert'); - $this->log('获取证书列表成功(total=' . $data['total'] . ')'); - } catch (Exception $e) { - throw new Exception('获取证书列表失败:' . $e->getMessage()); - } - - $success = 0; - $errmsg = null; - foreach ($data['nodes'] as $row) { - if (empty($row['domains'])) continue; - $flag = false; - foreach ($row['domains'] as $domain) { - if (in_array($domain, $domains) || in_array('*' . substr($domain, strpos($domain, '.')), $domains)) { - $flag = true; - break; - } - } - if ($flag) { - $params = [ - 'id' => $row['id'], - 'manual' => [ - 'crt' => $fullchain, - 'key' => $privatekey, - ], - 'type' => 2, - ]; - try { - $this->request('/api/open/cert', $params); - $this->log("证书ID:{$row['id']}更新成功!"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("证书ID:{$row['id']}更新失败:" . $errmsg); - } - } - } - if ($success == 0) { - $params = [ - 'manual' => [ - 'crt' => $fullchain, - 'key' => $privatekey, - ], - 'type' => 2, - ]; - $this->request('/api/open/cert', $params); - $this->log("证书上传成功!"); - } - } - - private function request($path, $params = null) - { - $url = $this->url . $path; - $headers = ['X-SLCE-API-TOKEN' => $this->token]; - $body = null; - if ($params) { - $headers['Content-Type'] = 'application/json'; - $body = json_encode($params); - } - $response = http_request($url, $body, null, null, $headers, $this->proxy); - $result = json_decode($response['body'], true); - if ($response['code'] == 200 && $result) { - return $result['data'] ?? null; - } else { - throw new Exception(!empty($result['msg']) ? $result['msg'] : '请求失败(httpCode=' . $response['code'] . ')'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +url = rtrim($config['url'], '/'); + $this->token = $config['token']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->token)) throw new Exception('请填写控制台地址和API Token'); + $this->request('/api/open/system'); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domains = $config['domainList']; + if (empty($domains)) throw new Exception('没有设置要部署的域名'); + + try { + $data = $this->request('/api/open/cert'); + $this->log('获取证书列表成功(total=' . $data['total'] . ')'); + } catch (Exception $e) { + throw new Exception('获取证书列表失败:' . $e->getMessage()); + } + + $success = 0; + $errmsg = null; + foreach ($data['nodes'] as $row) { + if (empty($row['domains'])) continue; + $flag = false; + foreach ($row['domains'] as $domain) { + if (in_array($domain, $domains) || in_array('*' . substr($domain, strpos($domain, '.')), $domains)) { + $flag = true; + break; + } + } + if ($flag) { + $params = [ + 'id' => $row['id'], + 'manual' => [ + 'crt' => $fullchain, + 'key' => $privatekey, + ], + 'type' => 2, + ]; + try { + $this->request('/api/open/cert', $params); + $this->log("证书ID:{$row['id']}更新成功!"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("证书ID:{$row['id']}更新失败:" . $errmsg); + } + } + } + if ($success == 0) { + $params = [ + 'manual' => [ + 'crt' => $fullchain, + 'key' => $privatekey, + ], + 'type' => 2, + ]; + $this->request('/api/open/cert', $params); + $this->log("证书上传成功!"); + } + } + + private function request($path, $params = null) + { + $url = $this->url . $path; + $headers = ['X-SLCE-API-TOKEN' => $this->token]; + $body = null; + if ($params) { + $headers['Content-Type'] = 'application/json'; + $body = json_encode($params); + } + $response = http_request($url, $body, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if ($response['code'] == 200 && $result) { + return $result['data'] ?? null; + } else { + throw new Exception(!empty($result['msg']) ? $result['msg'] : '请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/ssh.php b/app/lib/deploy/ssh.php index fff72ec..241ad99 100644 --- a/app/lib/deploy/ssh.php +++ b/app/lib/deploy/ssh.php @@ -1,213 +1,213 @@ -config = $config; - } - - public function check() - { - $this->connect(); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $connection = $this->connect(); - if (isset($config['cmd_pre']) && !empty($config['cmd_pre'])) { - $cmds = explode("\n", $config['cmd_pre']); - foreach ($cmds as $cmd) { - $cmd = trim($cmd); - if (empty($cmd)) continue; - $this->exec($connection, $cmd); - } - } - $sftp = ssh2_sftp($connection); - if ($config['format'] == 'pem') { - $stream = fopen("ssh2.sftp://$sftp{$config['pem_cert_file']}", 'w'); - if (!$stream) { - throw new Exception("无法创建证书文件:{$config['pem_cert_file']}"); - } - fwrite($stream, $fullchain); - fclose($stream); - $this->log('证书已保存到:' . $config['pem_cert_file']); - - $stream = fopen("ssh2.sftp://$sftp{$config['pem_key_file']}", 'w'); - if (!$stream) { - throw new Exception("无法创建私钥文件:{$config['pem_key_file']}"); - } - fwrite($stream, $privatekey); - fclose($stream); - $this->log('私钥已保存到:' . $config['pem_key_file']); - } elseif ($config['format'] == 'pfx') { - $pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null); - - $stream = fopen("ssh2.sftp://$sftp{$config['pfx_file']}", 'w'); - if (!$stream) { - throw new Exception("无法创建PFX证书文件:{$config['pfx_file']}"); - } - fwrite($stream, $pfx); - fclose($stream); - $this->log('PFX证书已保存到:' . $config['pfx_file']); - - if ($config['uptype'] == '1' && !empty($config['iis_domain'])) { - $cert_hash = openssl_x509_fingerprint($fullchain, 'sha1'); - $this->deploy_iis($connection, $config['iis_domain'], $config['pfx_file'], $config['pfx_pass'], $cert_hash); - $config['cmd'] = null; - } - } - if (!empty($config['cmd'])) { - $cmds = explode("\n", $config['cmd']); - foreach ($cmds as $cmd) { - $cmd = trim($cmd); - if (empty($cmd)) continue; - $this->exec($connection, $cmd); - } - } - } - - private function deploy_iis($connection, $domain, $pfx_file, $pfx_pass, $cert_hash) - { - if (!strpos($domain, ':')) { - $domain .= ':443'; - } - $ret = $this->exec($connection, 'netsh http show sslcert hostnameport=' . $domain); - if (preg_match('/:\s+(\w{40})/', $ret, $match)) { - if ($match[1] == $cert_hash) { - $this->log('IIS域名 ' . $domain . ' 证书已存在,无需更新'); - return; - } - } - $p = '-p ""'; - if (!empty($pfx_pass)) $p = '-p ' . $pfx_pass; - if (substr($pfx_file, 0, 1) == '/') $pfx_file = substr($pfx_file, 1); - $this->exec($connection, 'certutil ' . $p . ' -importPFX ' . $pfx_file); - $this->exec($connection, 'netsh http delete sslcert hostnameport=' . $domain); - $this->exec($connection, 'netsh http add sslcert hostnameport=' . $domain . ' certhash=' . $cert_hash . ' certstorename=MY appid=\'{' . $this->uuid() . '}\''); - $this->log('IIS域名 ' . $domain . ' 证书已更新'); - } - - private function uuid() - { - $guid = md5(uniqid(mt_rand(), true)); - return substr($guid, 0, 8) . '-' . substr($guid, 8, 4) . '-4' . substr($guid, 12, 3) . '-' . substr($guid, 16, 4) . '-' . substr($guid, 20, 12); - } - - private function exec($connection, $cmd) - { - $this->log('执行命令:' . $cmd); - $stream = ssh2_exec($connection, $cmd); - $errorStream = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR); - if (!$stream || !$errorStream) { - throw new Exception('执行命令失败'); - } - stream_set_blocking($stream, true); - stream_set_blocking($errorStream, true); - $output = stream_get_contents($stream); - $errorOutput = stream_get_contents($errorStream); - fclose($stream); - fclose($errorStream); - if (trim($errorOutput)) { - if ($this->config['windows'] == '1' && $this->containsGBKChinese($errorOutput)) { - $errorOutput = mb_convert_encoding($errorOutput, 'UTF-8', 'GBK'); - } - throw new Exception('执行命令失败:' . trim($errorOutput)); - } else { - if ($this->config['windows'] == '1' && $this->containsGBKChinese($output)) { - $output = mb_convert_encoding($output, 'UTF-8', 'GBK'); - } - $this->log('执行命令成功:' . trim($output)); - return $output; - } - } - - private function connect() - { - if (!function_exists('ssh2_connect')) { - throw new Exception('ssh2扩展未安装'); - } - if (empty($this->config['host']) || empty($this->config['port']) || empty($this->config['username']) || $this->config['auth'] == '0' && empty($this->config['password']) || $this->config['auth'] == '1' && empty($this->config['privatekey'])) { - throw new Exception('必填参数不能为空'); - } - if (!filter_var($this->config['host'], FILTER_VALIDATE_IP) && !filter_var($this->config['host'], FILTER_VALIDATE_DOMAIN)) { - throw new Exception('主机地址不合法'); - } - if (!is_numeric($this->config['port']) || $this->config['port'] < 1 || $this->config['port'] > 65535) { - throw new Exception('端口不合法'); - } - - $connection = ssh2_connect($this->config['host'], intval($this->config['port'])); - if (!$connection) { - throw new Exception('SSH连接失败'); - } - if ($this->config['auth'] == '1') { - $publicKey = $this->getPublicKey($this->config['privatekey']); - $publicKeyPath = app()->getRuntimePath() . $this->config['host'] . '.pub'; - $privateKeyPath = app()->getRuntimePath() . $this->config['host'] . '.key'; - $umask = umask(0066); - file_put_contents($privateKeyPath, $this->config['privatekey']); - file_put_contents($publicKeyPath, $publicKey); - umask($umask); - if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) { - throw new Exception('私钥认证失败'); - } - } else { - if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) { - throw new Exception('用户名或密码错误'); - } - } - return $connection; - } - - private function getPublicKey($privateKey) - { - $res = openssl_pkey_get_private($privateKey); - if (!$res) { - throw new Exception('加载私钥失败'); - } - $details = openssl_pkey_get_details($res); - if (!$details || !isset($details['key'])) { - throw new Exception('从私钥导出公钥失败'); - } - $buffer = pack("N", 7) . "ssh-rsa" . - $this->sshEncodeBuffer($details['rsa']['e']) . - $this->sshEncodeBuffer($details['rsa']['n']); - return "ssh-rsa " . base64_encode($buffer); - } - - private function sshEncodeBuffer($buffer) - { - $len = strlen($buffer); - if (ord($buffer[0]) & 0x80) { - $len++; - $buffer = "\x00" . $buffer; - } - return pack("Na*", $len, $buffer); - } - - private function containsGBKChinese($string) - { - return preg_match('/[\x81-\xFE][\x40-\xFE]/', $string) === 1; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - public function setLogger($logger) - { - $this->logger = $logger; - } -} +config = $config; + } + + public function check() + { + $this->connect(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $connection = $this->connect(); + if (isset($config['cmd_pre']) && !empty($config['cmd_pre'])) { + $cmds = explode("\n", $config['cmd_pre']); + foreach ($cmds as $cmd) { + $cmd = trim($cmd); + if (empty($cmd)) continue; + $this->exec($connection, $cmd); + } + } + $sftp = ssh2_sftp($connection); + if ($config['format'] == 'pem') { + $stream = fopen("ssh2.sftp://$sftp{$config['pem_cert_file']}", 'w'); + if (!$stream) { + throw new Exception("无法创建证书文件:{$config['pem_cert_file']}"); + } + fwrite($stream, $fullchain); + fclose($stream); + $this->log('证书已保存到:' . $config['pem_cert_file']); + + $stream = fopen("ssh2.sftp://$sftp{$config['pem_key_file']}", 'w'); + if (!$stream) { + throw new Exception("无法创建私钥文件:{$config['pem_key_file']}"); + } + fwrite($stream, $privatekey); + fclose($stream); + $this->log('私钥已保存到:' . $config['pem_key_file']); + } elseif ($config['format'] == 'pfx') { + $pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null); + + $stream = fopen("ssh2.sftp://$sftp{$config['pfx_file']}", 'w'); + if (!$stream) { + throw new Exception("无法创建PFX证书文件:{$config['pfx_file']}"); + } + fwrite($stream, $pfx); + fclose($stream); + $this->log('PFX证书已保存到:' . $config['pfx_file']); + + if ($config['uptype'] == '1' && !empty($config['iis_domain'])) { + $cert_hash = openssl_x509_fingerprint($fullchain, 'sha1'); + $this->deploy_iis($connection, $config['iis_domain'], $config['pfx_file'], $config['pfx_pass'], $cert_hash); + $config['cmd'] = null; + } + } + if (!empty($config['cmd'])) { + $cmds = explode("\n", $config['cmd']); + foreach ($cmds as $cmd) { + $cmd = trim($cmd); + if (empty($cmd)) continue; + $this->exec($connection, $cmd); + } + } + } + + private function deploy_iis($connection, $domain, $pfx_file, $pfx_pass, $cert_hash) + { + if (!strpos($domain, ':')) { + $domain .= ':443'; + } + $ret = $this->exec($connection, 'netsh http show sslcert hostnameport=' . $domain); + if (preg_match('/:\s+(\w{40})/', $ret, $match)) { + if ($match[1] == $cert_hash) { + $this->log('IIS域名 ' . $domain . ' 证书已存在,无需更新'); + return; + } + } + $p = '-p ""'; + if (!empty($pfx_pass)) $p = '-p ' . $pfx_pass; + if (substr($pfx_file, 0, 1) == '/') $pfx_file = substr($pfx_file, 1); + $this->exec($connection, 'certutil ' . $p . ' -importPFX ' . $pfx_file); + $this->exec($connection, 'netsh http delete sslcert hostnameport=' . $domain); + $this->exec($connection, 'netsh http add sslcert hostnameport=' . $domain . ' certhash=' . $cert_hash . ' certstorename=MY appid=\'{' . $this->uuid() . '}\''); + $this->log('IIS域名 ' . $domain . ' 证书已更新'); + } + + private function uuid() + { + $guid = md5(uniqid(mt_rand(), true)); + return substr($guid, 0, 8) . '-' . substr($guid, 8, 4) . '-4' . substr($guid, 12, 3) . '-' . substr($guid, 16, 4) . '-' . substr($guid, 20, 12); + } + + private function exec($connection, $cmd) + { + $this->log('执行命令:' . $cmd); + $stream = ssh2_exec($connection, $cmd); + $errorStream = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR); + if (!$stream || !$errorStream) { + throw new Exception('执行命令失败'); + } + stream_set_blocking($stream, true); + stream_set_blocking($errorStream, true); + $output = stream_get_contents($stream); + $errorOutput = stream_get_contents($errorStream); + fclose($stream); + fclose($errorStream); + if (trim($errorOutput)) { + if ($this->config['windows'] == '1' && $this->containsGBKChinese($errorOutput)) { + $errorOutput = mb_convert_encoding($errorOutput, 'UTF-8', 'GBK'); + } + throw new Exception('执行命令失败:' . trim($errorOutput)); + } else { + if ($this->config['windows'] == '1' && $this->containsGBKChinese($output)) { + $output = mb_convert_encoding($output, 'UTF-8', 'GBK'); + } + $this->log('执行命令成功:' . trim($output)); + return $output; + } + } + + private function connect() + { + if (!function_exists('ssh2_connect')) { + throw new Exception('ssh2扩展未安装'); + } + if (empty($this->config['host']) || empty($this->config['port']) || empty($this->config['username']) || $this->config['auth'] == '0' && empty($this->config['password']) || $this->config['auth'] == '1' && empty($this->config['privatekey'])) { + throw new Exception('必填参数不能为空'); + } + if (!filter_var($this->config['host'], FILTER_VALIDATE_IP) && !filter_var($this->config['host'], FILTER_VALIDATE_DOMAIN)) { + throw new Exception('主机地址不合法'); + } + if (!is_numeric($this->config['port']) || $this->config['port'] < 1 || $this->config['port'] > 65535) { + throw new Exception('端口不合法'); + } + + $connection = ssh2_connect($this->config['host'], intval($this->config['port'])); + if (!$connection) { + throw new Exception('SSH连接失败'); + } + if ($this->config['auth'] == '1') { + $publicKey = $this->getPublicKey($this->config['privatekey']); + $publicKeyPath = app()->getRuntimePath() . $this->config['host'] . '.pub'; + $privateKeyPath = app()->getRuntimePath() . $this->config['host'] . '.key'; + $umask = umask(0066); + file_put_contents($privateKeyPath, $this->config['privatekey']); + file_put_contents($publicKeyPath, $publicKey); + umask($umask); + if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) { + throw new Exception('私钥认证失败'); + } + } else { + if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) { + throw new Exception('用户名或密码错误'); + } + } + return $connection; + } + + private function getPublicKey($privateKey) + { + $res = openssl_pkey_get_private($privateKey); + if (!$res) { + throw new Exception('加载私钥失败'); + } + $details = openssl_pkey_get_details($res); + if (!$details || !isset($details['key'])) { + throw new Exception('从私钥导出公钥失败'); + } + $buffer = pack("N", 7) . "ssh-rsa" . + $this->sshEncodeBuffer($details['rsa']['e']) . + $this->sshEncodeBuffer($details['rsa']['n']); + return "ssh-rsa " . base64_encode($buffer); + } + + private function sshEncodeBuffer($buffer) + { + $len = strlen($buffer); + if (ord($buffer[0]) & 0x80) { + $len++; + $buffer = "\x00" . $buffer; + } + return pack("Na*", $len, $buffer); + } + + private function containsGBKChinese($string) + { + return preg_match('/[\x81-\xFE][\x40-\xFE]/', $string) === 1; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + public function setLogger($logger) + { + $this->logger = $logger; + } +} diff --git a/app/lib/deploy/synology.php b/app/lib/deploy/synology.php index b0434a8..e18e031 100644 --- a/app/lib/deploy/synology.php +++ b/app/lib/deploy/synology.php @@ -1,167 +1,167 @@ -url = rtrim($config['url'], '/'); - $this->username = $config['username']; - $this->password = $config['password']; - $this->version = $config['version']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->username) || empty($this->password)) throw new Exception('必填内容不能为空'); - $this->login(); - } - - private function login() - { - $url = $this->url . '/webapi/' . ($this->version == '1' ? 'auth.cgi' : 'entry.cgi'); - $params = [ - 'api' => 'SYNO.API.Auth', - 'version' => 6, - 'method' => 'login', - 'session' => 'webui', - 'account' => $this->username, - 'passwd' => $this->password, - 'format' => 'sid', - 'enable_syno_token' => 'yes', - ]; - $response = http_request($url, http_build_query($params), null, null, null, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['success']) && $result['success']) { - $this->token = $result['data']; - } elseif (isset($result['error'])) { - throw new Exception('登录失败:' . $result['error']); - } else { - throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); - } - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $this->login(); - $certInfo = openssl_x509_parse($fullchain, true); - $certInfo['validFrom_time_t']; - if (!$certInfo) throw new Exception('证书解析失败'); - - $url = $this->url . '/webapi/entry.cgi'; - $params = [ - 'api' => 'SYNO.Core.Certificate.CRT', - 'version' => 1, - 'method' => 'list', - '_sid' => $this->token['sid'], - 'SynoToken' => $this->token['synotoken'], - ]; - $response = http_request($url . '?' . http_build_query($params), null, null, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['success']) && $result['success']) { - $this->log('获取证书列表成功'); - } elseif (isset($result['error'])) { - throw new Exception('获取证书列表失败:' . json_encode($result['error'])); - } else { - throw new Exception('获取证书列表失败(httpCode=' . $response['code'] . ')'); - } - - $id = null; - $validFrom = 0; - foreach ($result['data']['certificates'] as $certificate) { - if ($certificate['subject']['common_name'] == $certInfo['subject']['CN'] || $certificate['desc'] == $config['desc']) { - $id = $certificate['id']; - $validFrom = \DateTime::createFromFormat('M d H:i:s Y T', $certificate['valid_from'])->getTimestamp(); - break; - } - } - if ($id) { - if ($validFrom == $certInfo['validFrom_time_t']) { - $this->log('证书ID:' . $id . '已存在,无需更新'); - return; - } - $this->import($fullchain, $privatekey, $config, $id); - } else { - $this->import($fullchain, $privatekey, $config); - } - } - - private function import($fullchain, $privatekey, $config, $id = null) - { - $url = $this->url . '/webapi/entry.cgi'; - $params = [ - 'api' => 'SYNO.Core.Certificate', - 'version' => 1, - 'method' => 'import', - '_sid' => $this->token['sid'], - 'SynoToken' => $this->token['synotoken'], - ]; - $headers = [ - 'Content-Type' => 'multipart/form-data' - ]; - $post = [ - [ - 'name' => 'key', - 'filename' => 'key.pem', - 'contents' => $privatekey - ], - [ - 'name' => 'cert', - 'filename' => 'cert.pem', - 'contents' => $fullchain - ], - [ - 'name' => 'id', - 'contents' => $id - ], - [ - 'name' => 'desc', - 'contents' => $config['desc'] - ] - ]; - $response = http_request($url . '?' . http_build_query($params), $post, null, null, $headers, $this->proxy, null, 15); - $result = json_decode($response['body'], true); - if ($id) { - if (isset($result['success']) && $result['success']) { - $this->log('证书ID:' . $id . '更新成功!'); - } elseif (isset($result['error'])) { - throw new Exception('证书ID:' . $id . '更新失败:' . json_encode($result['error'])); - } else { - throw new Exception('证书ID:' . $id . '更新失败(httpCode=' . $response['code'] . ')'); - } - } else { - if (isset($result['success']) && $result['success']) { - $this->log('证书上传成功!'); - } elseif (isset($result['error'])) { - throw new Exception('证书上传失败:' . json_encode($result['error'])); - } else { - throw new Exception('证书上传失败(httpCode=' . $response['code'] . ')'); - } - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +url = rtrim($config['url'], '/'); + $this->username = $config['username']; + $this->password = $config['password']; + $this->version = $config['version']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->username) || empty($this->password)) throw new Exception('必填内容不能为空'); + $this->login(); + } + + private function login() + { + $url = $this->url . '/webapi/' . ($this->version == '1' ? 'auth.cgi' : 'entry.cgi'); + $params = [ + 'api' => 'SYNO.API.Auth', + 'version' => 6, + 'method' => 'login', + 'session' => 'webui', + 'account' => $this->username, + 'passwd' => $this->password, + 'format' => 'sid', + 'enable_syno_token' => 'yes', + ]; + $response = http_request($url, http_build_query($params), null, null, null, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['success']) && $result['success']) { + $this->token = $result['data']; + } elseif (isset($result['error'])) { + throw new Exception('登录失败:' . $result['error']); + } else { + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $this->login(); + $certInfo = openssl_x509_parse($fullchain, true); + $certInfo['validFrom_time_t']; + if (!$certInfo) throw new Exception('证书解析失败'); + + $url = $this->url . '/webapi/entry.cgi'; + $params = [ + 'api' => 'SYNO.Core.Certificate.CRT', + 'version' => 1, + 'method' => 'list', + '_sid' => $this->token['sid'], + 'SynoToken' => $this->token['synotoken'], + ]; + $response = http_request($url . '?' . http_build_query($params), null, null, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['success']) && $result['success']) { + $this->log('获取证书列表成功'); + } elseif (isset($result['error'])) { + throw new Exception('获取证书列表失败:' . json_encode($result['error'])); + } else { + throw new Exception('获取证书列表失败(httpCode=' . $response['code'] . ')'); + } + + $id = null; + $validFrom = 0; + foreach ($result['data']['certificates'] as $certificate) { + if ($certificate['subject']['common_name'] == $certInfo['subject']['CN'] || $certificate['desc'] == $config['desc']) { + $id = $certificate['id']; + $validFrom = \DateTime::createFromFormat('M d H:i:s Y T', $certificate['valid_from'])->getTimestamp(); + break; + } + } + if ($id) { + if ($validFrom == $certInfo['validFrom_time_t']) { + $this->log('证书ID:' . $id . '已存在,无需更新'); + return; + } + $this->import($fullchain, $privatekey, $config, $id); + } else { + $this->import($fullchain, $privatekey, $config); + } + } + + private function import($fullchain, $privatekey, $config, $id = null) + { + $url = $this->url . '/webapi/entry.cgi'; + $params = [ + 'api' => 'SYNO.Core.Certificate', + 'version' => 1, + 'method' => 'import', + '_sid' => $this->token['sid'], + 'SynoToken' => $this->token['synotoken'], + ]; + $headers = [ + 'Content-Type' => 'multipart/form-data' + ]; + $post = [ + [ + 'name' => 'key', + 'filename' => 'key.pem', + 'contents' => $privatekey + ], + [ + 'name' => 'cert', + 'filename' => 'cert.pem', + 'contents' => $fullchain + ], + [ + 'name' => 'id', + 'contents' => $id + ], + [ + 'name' => 'desc', + 'contents' => $config['desc'] + ] + ]; + $response = http_request($url . '?' . http_build_query($params), $post, null, null, $headers, $this->proxy, null, 15); + $result = json_decode($response['body'], true); + if ($id) { + if (isset($result['success']) && $result['success']) { + $this->log('证书ID:' . $id . '更新成功!'); + } elseif (isset($result['error'])) { + throw new Exception('证书ID:' . $id . '更新失败:' . json_encode($result['error'])); + } else { + throw new Exception('证书ID:' . $id . '更新失败(httpCode=' . $response['code'] . ')'); + } + } else { + if (isset($result['success']) && $result['success']) { + $this->log('证书上传成功!'); + } elseif (isset($result['error'])) { + throw new Exception('证书上传失败:' . json_encode($result['error'])); + } else { + throw new Exception('证书上传失败(httpCode=' . $response['code'] . ')'); + } + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/tencent.php b/app/lib/deploy/tencent.php index f7fe602..f290288 100644 --- a/app/lib/deploy/tencent.php +++ b/app/lib/deploy/tencent.php @@ -1,295 +1,295 @@ -SecretId = $config['SecretId']; - $this->SecretKey = $config['SecretKey']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', null, $this->proxy); - } - - public function check() - { - if (empty($this->SecretId) || empty($this->SecretKey)) throw new Exception('必填参数不能为空'); - $this->client->request('DescribeCertificates', []); - return true; - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $cert_id = $this->get_cert_id($fullchain, $privatekey); - if (!$cert_id) throw new Exception('证书ID获取失败'); - if ($config['product'] == 'cos') { - if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); - if (empty($config['cos_bucket'])) throw new Exception('存储桶名称不能为空'); - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $instance_id = $config['regionid'] . '|' . $config['cos_bucket'] . '|' . $config['domain']; - $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid'], $this->proxy); - } elseif ($config['product'] == 'tke') { - if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); - if (empty($config['tke_cluster_id'])) throw new Exception('集群ID不能为空'); - if (empty($config['tke_namespace'])) throw new Exception('命名空间不能为空'); - if (empty($config['tke_secret'])) throw new Exception('secret名称不能为空'); - $instance_id = $config['tke_cluster_id'] . '|' . $config['tke_namespace'] . '|' . $config['tke_secret']; - $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid'], $this->proxy); - } elseif ($config['product'] == 'lighthouse') { - if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); - if (empty($config['lighthouse_id'])) throw new Exception('实例ID不能为空'); - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $instance_id = $config['regionid'] . '|' . $config['lighthouse_id'] . '|' . $config['domain']; - $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid'], $this->proxy); - } elseif ($config['product'] == 'ddos') { - if (empty($config['lighthouse_id'])) throw new Exception('实例ID不能为空'); - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $instance_id = $config['lighthouse_id'] . '|' . $config['domain'] . '|443'; - } elseif ($config['product'] == 'clb') { - return $this->deploy_clb($cert_id, $config); - } elseif ($config['product'] == 'scf') { - return $this->deploy_scf($cert_id, $config); - } elseif ($config['product'] == 'teo' && isset($config['site_id'])) { - return $this->deploy_teo($cert_id, $config); - } else { - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - if ($config['product'] == 'waf') { - $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['region'], $this->proxy); - } elseif (in_array($config['product'], ['tse', 'scf'])) { - if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); - $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid'], $this->proxy); - } - $instance_id = $config['domain']; - } - 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'])) { - if ($this->deploy_query($info['record_id'])) { - $this->log(strtoupper($config['product']) . '实例 ' . $instance_id . ' 已部署证书,无需重复部署'); - return; - } - } - throw $e; - } - } - - private function get_cert_id($fullchain, $privatekey) - { - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $param = [ - 'CertificatePublicKey' => $fullchain, - 'CertificatePrivateKey' => $privatekey, - 'CertificateType' => 'SVR', - 'Alias' => $cert_name, - 'Repeatable' => false, - ]; - try { - $data = $this->client->request('UploadCertificate', $param); - } catch (Exception $e) { - throw new Exception('上传证书失败:' . $e->getMessage()); - } - $this->log('上传证书成功 CertificateId=' . $data['CertificateId']); - usleep(300000); - - $param = [ - 'CertificateIds' => [$data['CertificateId']], - 'SwitchStatus' => 1, - ]; - $this->client->request('ModifyCertificatesExpiringNotificationSwitch', $param); - - return $data['CertificateId']; - } - - private function deploy_common($product, $cert_id, $instance_id) - { - if (in_array($product, ['cdn', 'waf', 'teo', 'ddos', 'live', 'vod']) && strpos($instance_id, ',') !== false) { - $instance_ids = explode(',', $instance_id); - } else { - $instance_ids = [$instance_id]; - } - if ($product == 'cdn') { - $instance_ids = array_map(function ($id) { - return $id . '|on'; - }, $instance_ids); - } - $param = [ - 'CertificateId' => $cert_id, - 'InstanceIdList' => $instance_ids, - 'ResourceType' => $product, - ]; - if ($product == 'live') $param['Status'] = 1; - $data = $this->client->request('DeployCertificateInstance', $param); - $this->log(json_encode($data)); - $this->log(strtoupper($product) . '实例 ' . $instance_id . ' 部署证书成功!'); - return $data['DeployRecordId']; - } - - private function deploy_query($record_id) - { - $param = [ - 'DeployRecordId' => strval($record_id), - ]; - try { - $data = $this->client->request('DescribeHostDeployRecordDetail', $param); - if (isset($data['SuccessTotalCount']) && $data['SuccessTotalCount'] >= 1 || isset($data['RunningTotalCount']) && $data['RunningTotalCount'] >= 1) { - return true; - } - if (isset($data['FailedTotalCount']) && $data['FailedTotalCount'] >= 1 && !empty($data['DeployRecordDetailList'])) { - $errmsg = $data['DeployRecordDetailList'][0]['ErrorMsg']; - if (strpos($errmsg, '\u')) { - $errmsg = json_decode($errmsg); - } - $this->log('证书部署失败原因:' . $errmsg); - } - } catch (Exception $e) { - $this->log('查询证书部署记录失败:' . $e->getMessage()); - } - return false; - } - - private function deploy_clb($cert_id, $config) - { - if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); - if (empty($config['clb_id'])) throw new Exception('负载均衡ID不能为空'); - $sni_switch = !empty($config['clb_domain']) ? 1 : 0; - - $client = new TencentCloud($this->SecretId, $this->SecretKey, 'clb.tencentcloudapi.com', 'clb', '2018-03-17', $config['regionid'], $this->proxy); - $param = [ - 'LoadBalancerId' => $config['clb_id'], - 'Protocol' => 'HTTPS', - ]; - if (!empty($config['clb_listener_id'])) { - $param['ListenerIds'] = [$config['clb_listener_id']]; - } - try { - $data = $client->request('DescribeListeners', $param); - } catch (Exception $e) { - throw new Exception('获取监听器列表失败:' . $e->getMessage()); - } - if (!isset($data['TotalCount']) || $data['TotalCount'] == 0) throw new Exception('负载均衡:' . $config['clb_id'] . '监听器列表为空'); - $count = 0; - foreach ($data['Listeners'] as $listener) { - if ($listener['SniSwitch'] == $sni_switch) { - if ($sni_switch == 1) { - foreach ($listener['Rules'] as $rule) { - if ($rule['Domain'] == $config['clb_domain']) { - if (isset($rule['Certificate']['CertId']) && $cert_id == $rule['Certificate']['CertId']) { - $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 域名 ' . $rule['Domain'] . ' 已部署证书,无需重复部署'); - } else { - $param = [ - 'LoadBalancerId' => $config['clb_id'], - 'ListenerId' => $listener['ListenerId'], - 'Domain' => $rule['Domain'], - 'Certificate' => [ - 'SSLMode' => 'UNIDIRECTIONAL', - 'CertId' => $cert_id, - ], - ]; - $client->request('ModifyDomainAttributes', $param); - $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 域名 ' . $rule['Domain'] . ' 部署证书成功!'); - } - $count++; - } - } - } else { - if (isset($listener['Certificate']['CertId']) && $cert_id == $listener['Certificate']['CertId']) { - $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 已部署证书,无需重复部署'); - } else { - $param = [ - 'LoadBalancerId' => $config['clb_id'], - 'ListenerId' => $listener['ListenerId'], - 'Certificate' => [ - 'SSLMode' => 'UNIDIRECTIONAL', - 'CertId' => $cert_id, - ], - ]; - $client->request('ModifyListener', $param); - $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 部署证书成功!'); - } - $count++; - } - } - } - if ($count == 0) throw new Exception('没有找到要更新证书的监听器'); - } - - private function deploy_scf($cert_id, $config) - { - if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - - $client = new TencentCloud($this->SecretId, $this->SecretKey, 'scf.tencentcloudapi.com', 'scf', '2018-04-16', $config['regionid'], $this->proxy); - $param = [ - 'Domain' => $config['domain'], - ]; - try { - $data = $client->request('GetCustomDomain', $param); - } catch (Exception $e) { - throw new Exception('获取云函数自定义域名失败:' . $e->getMessage()); - } - - if (isset($data['CertConfig']['CertificateId']) && $data['CertConfig']['CertificateId'] == $cert_id) { - $this->log('云函数自定义域名 ' . $config['domain'] . ' 已部署证书,无需重复部署'); - return; - } - $data['CertConfig']['CertificateId'] = $cert_id; - if ($data['Protocol'] == 'HTTP') $data['Protocol'] = 'HTTP&HTTPS'; - - $param = [ - 'Domain' => $config['domain'], - 'Protocol' => $data['Protocol'], - 'CertConfig' => $data['CertConfig'], - ]; - $data = $client->request('UpdateCustomDomain', $param); - $this->log('云函数自定义域名 ' . $config['domain'] . ' 部署证书成功!'); - } - - private function deploy_teo($cert_id, $config) - { - if (empty($config['site_id'])) throw new Exception('站点ID不能为空'); - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - - $endpoint = isset($config['site_type']) && $config['site_type'] == 'intl' ? 'teo.intl.tencentcloudapi.com' : 'teo.tencentcloudapi.com'; - $client = new TencentCloud($this->SecretId, $this->SecretKey, $endpoint, 'teo', '2022-09-01', null, $this->proxy); - $hosts = explode(',', $config['domain']); - $param = [ - 'ZoneId' => $config['site_id'], - 'Hosts' => $hosts, - 'Mode' => 'sslcert', - 'ServerCertInfo' => [[ - 'CertId' => $cert_id - ]] - ]; - $data = $client->request('ModifyHostsCertificate', $param); - $this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!'); - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +SecretId = $config['SecretId']; + $this->SecretKey = $config['SecretKey']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', null, $this->proxy); + } + + public function check() + { + if (empty($this->SecretId) || empty($this->SecretKey)) throw new Exception('必填参数不能为空'); + $this->client->request('DescribeCertificates', []); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $cert_id = $this->get_cert_id($fullchain, $privatekey); + if (!$cert_id) throw new Exception('证书ID获取失败'); + if ($config['product'] == 'cos') { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + if (empty($config['cos_bucket'])) throw new Exception('存储桶名称不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $instance_id = $config['regionid'] . '|' . $config['cos_bucket'] . '|' . $config['domain']; + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid'], $this->proxy); + } elseif ($config['product'] == 'tke') { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + if (empty($config['tke_cluster_id'])) throw new Exception('集群ID不能为空'); + if (empty($config['tke_namespace'])) throw new Exception('命名空间不能为空'); + if (empty($config['tke_secret'])) throw new Exception('secret名称不能为空'); + $instance_id = $config['tke_cluster_id'] . '|' . $config['tke_namespace'] . '|' . $config['tke_secret']; + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid'], $this->proxy); + } elseif ($config['product'] == 'lighthouse') { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + if (empty($config['lighthouse_id'])) throw new Exception('实例ID不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $instance_id = $config['regionid'] . '|' . $config['lighthouse_id'] . '|' . $config['domain']; + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid'], $this->proxy); + } elseif ($config['product'] == 'ddos') { + if (empty($config['lighthouse_id'])) throw new Exception('实例ID不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $instance_id = $config['lighthouse_id'] . '|' . $config['domain'] . '|443'; + } elseif ($config['product'] == 'clb') { + return $this->deploy_clb($cert_id, $config); + } elseif ($config['product'] == 'scf') { + return $this->deploy_scf($cert_id, $config); + } elseif ($config['product'] == 'teo' && isset($config['site_id'])) { + return $this->deploy_teo($cert_id, $config); + } else { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + if ($config['product'] == 'waf') { + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['region'], $this->proxy); + } elseif (in_array($config['product'], ['tse', 'scf'])) { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid'], $this->proxy); + } + $instance_id = $config['domain']; + } + 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'])) { + if ($this->deploy_query($info['record_id'])) { + $this->log(strtoupper($config['product']) . '实例 ' . $instance_id . ' 已部署证书,无需重复部署'); + return; + } + } + throw $e; + } + } + + private function get_cert_id($fullchain, $privatekey) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $param = [ + 'CertificatePublicKey' => $fullchain, + 'CertificatePrivateKey' => $privatekey, + 'CertificateType' => 'SVR', + 'Alias' => $cert_name, + 'Repeatable' => false, + ]; + try { + $data = $this->client->request('UploadCertificate', $param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $this->log('上传证书成功 CertificateId=' . $data['CertificateId']); + usleep(300000); + + $param = [ + 'CertificateIds' => [$data['CertificateId']], + 'SwitchStatus' => 1, + ]; + $this->client->request('ModifyCertificatesExpiringNotificationSwitch', $param); + + return $data['CertificateId']; + } + + private function deploy_common($product, $cert_id, $instance_id) + { + if (in_array($product, ['cdn', 'waf', 'teo', 'ddos', 'live', 'vod']) && strpos($instance_id, ',') !== false) { + $instance_ids = explode(',', $instance_id); + } else { + $instance_ids = [$instance_id]; + } + if ($product == 'cdn') { + $instance_ids = array_map(function ($id) { + return $id . '|on'; + }, $instance_ids); + } + $param = [ + 'CertificateId' => $cert_id, + 'InstanceIdList' => $instance_ids, + 'ResourceType' => $product, + ]; + if ($product == 'live') $param['Status'] = 1; + $data = $this->client->request('DeployCertificateInstance', $param); + $this->log(json_encode($data)); + $this->log(strtoupper($product) . '实例 ' . $instance_id . ' 部署证书成功!'); + return $data['DeployRecordId']; + } + + private function deploy_query($record_id) + { + $param = [ + 'DeployRecordId' => strval($record_id), + ]; + try { + $data = $this->client->request('DescribeHostDeployRecordDetail', $param); + if (isset($data['SuccessTotalCount']) && $data['SuccessTotalCount'] >= 1 || isset($data['RunningTotalCount']) && $data['RunningTotalCount'] >= 1) { + return true; + } + if (isset($data['FailedTotalCount']) && $data['FailedTotalCount'] >= 1 && !empty($data['DeployRecordDetailList'])) { + $errmsg = $data['DeployRecordDetailList'][0]['ErrorMsg']; + if (strpos($errmsg, '\u')) { + $errmsg = json_decode($errmsg); + } + $this->log('证书部署失败原因:' . $errmsg); + } + } catch (Exception $e) { + $this->log('查询证书部署记录失败:' . $e->getMessage()); + } + return false; + } + + private function deploy_clb($cert_id, $config) + { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + if (empty($config['clb_id'])) throw new Exception('负载均衡ID不能为空'); + $sni_switch = !empty($config['clb_domain']) ? 1 : 0; + + $client = new TencentCloud($this->SecretId, $this->SecretKey, 'clb.tencentcloudapi.com', 'clb', '2018-03-17', $config['regionid'], $this->proxy); + $param = [ + 'LoadBalancerId' => $config['clb_id'], + 'Protocol' => 'HTTPS', + ]; + if (!empty($config['clb_listener_id'])) { + $param['ListenerIds'] = [$config['clb_listener_id']]; + } + try { + $data = $client->request('DescribeListeners', $param); + } catch (Exception $e) { + throw new Exception('获取监听器列表失败:' . $e->getMessage()); + } + if (!isset($data['TotalCount']) || $data['TotalCount'] == 0) throw new Exception('负载均衡:' . $config['clb_id'] . '监听器列表为空'); + $count = 0; + foreach ($data['Listeners'] as $listener) { + if ($listener['SniSwitch'] == $sni_switch) { + if ($sni_switch == 1) { + foreach ($listener['Rules'] as $rule) { + if ($rule['Domain'] == $config['clb_domain']) { + if (isset($rule['Certificate']['CertId']) && $cert_id == $rule['Certificate']['CertId']) { + $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 域名 ' . $rule['Domain'] . ' 已部署证书,无需重复部署'); + } else { + $param = [ + 'LoadBalancerId' => $config['clb_id'], + 'ListenerId' => $listener['ListenerId'], + 'Domain' => $rule['Domain'], + 'Certificate' => [ + 'SSLMode' => 'UNIDIRECTIONAL', + 'CertId' => $cert_id, + ], + ]; + $client->request('ModifyDomainAttributes', $param); + $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 域名 ' . $rule['Domain'] . ' 部署证书成功!'); + } + $count++; + } + } + } else { + if (isset($listener['Certificate']['CertId']) && $cert_id == $listener['Certificate']['CertId']) { + $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 已部署证书,无需重复部署'); + } else { + $param = [ + 'LoadBalancerId' => $config['clb_id'], + 'ListenerId' => $listener['ListenerId'], + 'Certificate' => [ + 'SSLMode' => 'UNIDIRECTIONAL', + 'CertId' => $cert_id, + ], + ]; + $client->request('ModifyListener', $param); + $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 部署证书成功!'); + } + $count++; + } + } + } + if ($count == 0) throw new Exception('没有找到要更新证书的监听器'); + } + + private function deploy_scf($cert_id, $config) + { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + + $client = new TencentCloud($this->SecretId, $this->SecretKey, 'scf.tencentcloudapi.com', 'scf', '2018-04-16', $config['regionid'], $this->proxy); + $param = [ + 'Domain' => $config['domain'], + ]; + try { + $data = $client->request('GetCustomDomain', $param); + } catch (Exception $e) { + throw new Exception('获取云函数自定义域名失败:' . $e->getMessage()); + } + + if (isset($data['CertConfig']['CertificateId']) && $data['CertConfig']['CertificateId'] == $cert_id) { + $this->log('云函数自定义域名 ' . $config['domain'] . ' 已部署证书,无需重复部署'); + return; + } + $data['CertConfig']['CertificateId'] = $cert_id; + if ($data['Protocol'] == 'HTTP') $data['Protocol'] = 'HTTP&HTTPS'; + + $param = [ + 'Domain' => $config['domain'], + 'Protocol' => $data['Protocol'], + 'CertConfig' => $data['CertConfig'], + ]; + $data = $client->request('UpdateCustomDomain', $param); + $this->log('云函数自定义域名 ' . $config['domain'] . ' 部署证书成功!'); + } + + private function deploy_teo($cert_id, $config) + { + if (empty($config['site_id'])) throw new Exception('站点ID不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + + $endpoint = isset($config['site_type']) && $config['site_type'] == 'intl' ? 'teo.intl.tencentcloudapi.com' : 'teo.tencentcloudapi.com'; + $client = new TencentCloud($this->SecretId, $this->SecretKey, $endpoint, 'teo', '2022-09-01', null, $this->proxy); + $hosts = explode(',', $config['domain']); + $param = [ + 'ZoneId' => $config['site_id'], + 'Hosts' => $hosts, + 'Mode' => 'sslcert', + 'ServerCertInfo' => [[ + 'CertId' => $cert_id + ]] + ]; + $data = $client->request('ModifyHostsCertificate', $param); + $this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!'); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/ucloud.php b/app/lib/deploy/ucloud.php index 1b8766b..b824a4f 100644 --- a/app/lib/deploy/ucloud.php +++ b/app/lib/deploy/ucloud.php @@ -1,118 +1,118 @@ -PublicKey = $config['PublicKey']; - $this->PrivateKey = $config['PrivateKey']; - $this->client = new UcloudClient($this->PublicKey, $this->PrivateKey); - } - - public function check() - { - if (empty($this->PublicKey) || empty($this->PrivateKey)) throw new Exception('必填参数不能为空'); - $param = ['Mode' => 'free']; - $this->client->request('GetCertificateList', $param); - return true; - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $domain_id = $config['domain_id']; - if (empty($domain_id)) throw new Exception('云分发资源ID不能为空'); - - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - $cert_name = str_replace(['*', '.'], '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - - $param = [ - 'CertName' => $cert_name, - 'UserCert' => $fullchain, - 'PrivateKey' => $privatekey, - ]; - try { - $data = $this->client->request('AddCertificate', $param); - $this->log('添加证书成功,名称:' . $cert_name); - } catch (Exception $e) { - if (strpos($e->getMessage(), 'cert already exist') !== false) { - $this->log('证书已存在,名称:' . $cert_name); - } else { - throw new Exception('添加证书失败 ' . $e->getMessage()); - } - } - - try { - $data = $this->client->request('GetUcdnDomainConfig', ['DomainId.0' => $domain_id]); - } catch (Exception $e) { - throw new Exception('获取加速域名配置失败 ' . $e->getMessage()); - } - if (empty($data['DomainList'])) throw new Exception('云分发资源ID:' . $domain_id . '不存在'); - $domain = $data['DomainList'][0]['Domain']; - $HttpsStatusCn = $data['DomainList'][0]['HttpsStatusCn']; - $HttpsStatusAbroad = $data['DomainList'][0]['HttpsStatusAbroad']; - - if ($data['DomainList'][0]['CertNameCn'] == $cert_name || $data['DomainList'][0]['CertNameAbroad'] == $cert_name) { - $this->log('云分发' . $domain_id . '证书已配置,无需重复操作'); - return; - } - - try { - $data = $this->client->request('GetCertificateBaseInfoList', ['Domain' => $domain]); - } catch (Exception $e) { - throw new Exception('获取可用证书列表失败 ' . $e->getMessage()); - } - if (empty($data['CertList'])) throw new Exception('可用证书列表为空'); - - $cert_id = null; - foreach ($data['CertList'] as $cert) { - if ($cert['CertName'] == $cert_name) { - $cert_id = $cert['CertId']; - break; - } - } - if (!$cert_id) throw new Exception('证书ID不存在'); - $this->log('证书ID获取成功:' . $cert_id); - - $param = [ - 'DomainId' => $domain_id, - 'CertName' => $cert_name, - 'CertId' => $cert_id, - 'CertType' => 'ucdn', - ]; - if ($HttpsStatusCn == 'enable') $param['HttpsStatusCn'] = $HttpsStatusCn; - if ($HttpsStatusAbroad == 'enable') $param['HttpsStatusAbroad'] = $HttpsStatusAbroad; - if ($HttpsStatusCn != 'enable' && $HttpsStatusAbroad != 'enable') $param['HttpsStatusCn'] = 'enable'; - try { - $data = $this->client->request('UpdateUcdnDomainHttpsConfigV2', $param); - } catch (Exception $e) { - throw new Exception('https加速配置失败 ' . $e->getMessage()); - } - $this->log('云分发' . $domain_id . '证书配置成功!'); - $info['cert_id'] = $cert_id; - $info['cert_name'] = $cert_name; - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +PublicKey = $config['PublicKey']; + $this->PrivateKey = $config['PrivateKey']; + $this->client = new UcloudClient($this->PublicKey, $this->PrivateKey); + } + + public function check() + { + if (empty($this->PublicKey) || empty($this->PrivateKey)) throw new Exception('必填参数不能为空'); + $param = ['Mode' => 'free']; + $this->client->request('GetCertificateList', $param); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domain_id = $config['domain_id']; + if (empty($domain_id)) throw new Exception('云分发资源ID不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace(['*', '.'], '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $param = [ + 'CertName' => $cert_name, + 'UserCert' => $fullchain, + 'PrivateKey' => $privatekey, + ]; + try { + $data = $this->client->request('AddCertificate', $param); + $this->log('添加证书成功,名称:' . $cert_name); + } catch (Exception $e) { + if (strpos($e->getMessage(), 'cert already exist') !== false) { + $this->log('证书已存在,名称:' . $cert_name); + } else { + throw new Exception('添加证书失败 ' . $e->getMessage()); + } + } + + try { + $data = $this->client->request('GetUcdnDomainConfig', ['DomainId.0' => $domain_id]); + } catch (Exception $e) { + throw new Exception('获取加速域名配置失败 ' . $e->getMessage()); + } + if (empty($data['DomainList'])) throw new Exception('云分发资源ID:' . $domain_id . '不存在'); + $domain = $data['DomainList'][0]['Domain']; + $HttpsStatusCn = $data['DomainList'][0]['HttpsStatusCn']; + $HttpsStatusAbroad = $data['DomainList'][0]['HttpsStatusAbroad']; + + if ($data['DomainList'][0]['CertNameCn'] == $cert_name || $data['DomainList'][0]['CertNameAbroad'] == $cert_name) { + $this->log('云分发' . $domain_id . '证书已配置,无需重复操作'); + return; + } + + try { + $data = $this->client->request('GetCertificateBaseInfoList', ['Domain' => $domain]); + } catch (Exception $e) { + throw new Exception('获取可用证书列表失败 ' . $e->getMessage()); + } + if (empty($data['CertList'])) throw new Exception('可用证书列表为空'); + + $cert_id = null; + foreach ($data['CertList'] as $cert) { + if ($cert['CertName'] == $cert_name) { + $cert_id = $cert['CertId']; + break; + } + } + if (!$cert_id) throw new Exception('证书ID不存在'); + $this->log('证书ID获取成功:' . $cert_id); + + $param = [ + 'DomainId' => $domain_id, + 'CertName' => $cert_name, + 'CertId' => $cert_id, + 'CertType' => 'ucdn', + ]; + if ($HttpsStatusCn == 'enable') $param['HttpsStatusCn'] = $HttpsStatusCn; + if ($HttpsStatusAbroad == 'enable') $param['HttpsStatusAbroad'] = $HttpsStatusAbroad; + if ($HttpsStatusCn != 'enable' && $HttpsStatusAbroad != 'enable') $param['HttpsStatusCn'] = 'enable'; + try { + $data = $this->client->request('UpdateUcdnDomainHttpsConfigV2', $param); + } catch (Exception $e) { + throw new Exception('https加速配置失败 ' . $e->getMessage()); + } + $this->log('云分发' . $domain_id . '证书配置成功!'); + $info['cert_id'] = $cert_id; + $info['cert_name'] = $cert_name; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/unicloud.php b/app/lib/deploy/unicloud.php index afb93d2..0e15e32 100644 --- a/app/lib/deploy/unicloud.php +++ b/app/lib/deploy/unicloud.php @@ -1,212 +1,212 @@ -username = $config['username']; - $this->password = $config['password']; - $this->proxy = $config['proxy'] == 1; - $this->deviceId = getMillisecond() . random(7, 1); - } - - public function check() - { - if (empty($this->username) || empty($this->password)) throw new Exception('账号或密码不能为空'); - $this->login(); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if (empty($config['domains'])) throw new Exception('绑定的域名不能为空'); - $this->getToken(); - - $url = 'https://unicloud-api.dcloud.net.cn/unicloud/api/host/create-domain-with-cert'; - foreach (explode(',', $config['domains']) as $domain) { - if (empty($domain)) continue; - $params = [ - 'appid' => '', - 'provider' => $config['provider'], - 'spaceId' => $config['spaceId'], - 'domain' => $domain, - 'cert' => rawurlencode($fullchain), - 'key' => rawurlencode($privatekey), - ]; - $post = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - $headers = [ - 'Token' => $this->token, - ]; - $response = http_request($url, $post, null, null, $headers, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['ret']) && $result['ret'] == 0) { - $this->log('域名:' . $domain . ' 证书更新成功!'); - } elseif(isset($result['desc'])) { - throw new Exception('域名:' . $domain . ' 证书更新失败:' . $result['desc']); - } else { - throw new Exception('域名:' . $domain . ' 证书更新失败:' . $response['body']); - } - } - } - - private function login() - { - $url = 'https://account.dcloud.net.cn/client'; - $clientInfo = $this->getClientInfo('__UNI__unicloud_console', '账号中心'); - $bizParams = [ - 'functionTarget' => 'uni-id-co', - 'functionArgs' => [ - 'method' => 'login', - 'params' => [[ - 'password' => $this->password, - 'captcha' => '', - 'resetAppId' => '__UNI__unicloud_console', - 'resetUniPlatform' => 'web', - 'isReturnToken' => false, - 'email' => $this->username, - ]], - 'clientInfo' => $clientInfo, - ], - ]; - $params = [ - 'method' => 'serverless.function.runtime.invoke', - 'params' => json_encode($bizParams, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'spaceId' => 'uni-id-server', - 'timestamp' => getMillisecond(), - ]; - $sign = $this->sign($params, 'ba461799-fde8-429f-8cc4-4b6d306e2339'); - $post = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - $headers = [ - 'Origin' => 'https://account.dcloud.net.cn', - 'Referer' => 'https://account.dcloud.net.cn/', - 'X-Client-Info' => json_encode($clientInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'X-Serverless-Sign' => $sign, - ]; - $response = http_request($url, $post, null, null, $headers, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['success']) && $result['success'] == true) { - if (isset($result['data']['errCode']) && $result['data']['errCode'] == 0) { - return $result['data']['newToken']['token']; - } else { - throw new Exception('登录失败:' . $result['data']['errMsg']); - } - } else { - throw new Exception('登录失败:' . $response['body']); - } - } - - private function getToken() - { - $uniIdToken = $this->login(); - $url = 'https://unicloud.dcloud.net.cn/client'; - $clientInfo = $this->getClientInfo('__UNI__unicloud_console', 'uniCloud控制台'); - $bizParams = [ - 'functionTarget' => 'uni-cloud-kernel', - 'functionArgs' => [ - 'action' => 'user/getUserToken', - 'data' => [ - 'isLogin' => true - ], - 'clientInfo' => $clientInfo, - 'uniIdToken' => $uniIdToken, - ], - ]; - $params = [ - 'method' => 'serverless.function.runtime.invoke', - 'params' => json_encode($bizParams, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'spaceId' => 'dc-6nfabcn6ada8d3dd', - 'timestamp' => getMillisecond(), - ]; - $sign = $this->sign($params, '4c1f7fbf-c732-42b0-ab10-4634a8bbe834'); - $post = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - $headers = [ - 'Origin' => 'https://account.dcloud.net.cn', - 'Referer' => 'https://account.dcloud.net.cn/', - 'X-Client-Info' => json_encode($clientInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'X-Client-Token' => $uniIdToken, - 'X-Serverless-Sign' => $sign, - ]; - $response = http_request($url, $post, null, null, $headers, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['success']) && $result['success'] == true) { - if (isset($result['data']['code']) && $result['data']['code'] == 0) { - if (isset($result['data']['data']['ret']) && $result['data']['data']['ret'] == 0) { - $this->token = $result['data']['data']['data']['token']; - return $result['data']['data']['data']['token']; - } else { - throw new Exception('获取token失败:' . $result['data']['data']['desc']); - } - } else { - throw new Exception('获取token失败:' . $response['body']); - } - } else { - throw new Exception('获取token失败:' . $response['body']); - } - } - - private function getClientInfo($appId, $appName, $appVersion = '1.0.0', $appVersionCode = '100') - { - $clientInfo = [ - 'PLATFORM' => 'web', - 'OS' => 'windows', - 'APPID' => $appId, - 'DEVICEID' => $this->deviceId, - 'scene' => 1001, - 'appId' => $appId, - 'appLanguage' => 'zh-Hans', - 'appName' => $appName, - 'appVersion' => $appVersion, - 'appVersionCode' => $appVersionCode, - 'browserName' => 'chrome', - 'browserVersion' => '122.0.6261.95', - 'deviceId' => $this->deviceId, - 'deviceModel' => 'PC', - 'deviceType' => 'pc', - 'hostName' => 'chrome', - 'hostVersion' => '122.0.6261.95', - 'osName' => 'windows', - 'osVersion' => '10 x64', - 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36', - 'uniCompilerVersion' => '4.45', - 'uniPlatform' => 'web', - 'uniRuntimeVersion' => '4.45', - 'locale' => 'zh-Hans', - 'LOCALE' => 'zh-Hans', - ]; - return $clientInfo; - } - - private function sign($data, $key) - { - ksort($data); - $signstr = ''; - foreach ($data as $k => $v) { - $signstr .= $k . '=' . $v . '&'; - } - $signstr = rtrim($signstr, '&'); - return hash_hmac('md5', $signstr, $key); - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +username = $config['username']; + $this->password = $config['password']; + $this->proxy = $config['proxy'] == 1; + $this->deviceId = getMillisecond() . random(7, 1); + } + + public function check() + { + if (empty($this->username) || empty($this->password)) throw new Exception('账号或密码不能为空'); + $this->login(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['domains'])) throw new Exception('绑定的域名不能为空'); + $this->getToken(); + + $url = 'https://unicloud-api.dcloud.net.cn/unicloud/api/host/create-domain-with-cert'; + foreach (explode(',', $config['domains']) as $domain) { + if (empty($domain)) continue; + $params = [ + 'appid' => '', + 'provider' => $config['provider'], + 'spaceId' => $config['spaceId'], + 'domain' => $domain, + 'cert' => rawurlencode($fullchain), + 'key' => rawurlencode($privatekey), + ]; + $post = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $headers = [ + 'Token' => $this->token, + ]; + $response = http_request($url, $post, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['ret']) && $result['ret'] == 0) { + $this->log('域名:' . $domain . ' 证书更新成功!'); + } elseif(isset($result['desc'])) { + throw new Exception('域名:' . $domain . ' 证书更新失败:' . $result['desc']); + } else { + throw new Exception('域名:' . $domain . ' 证书更新失败:' . $response['body']); + } + } + } + + private function login() + { + $url = 'https://account.dcloud.net.cn/client'; + $clientInfo = $this->getClientInfo('__UNI__unicloud_console', '账号中心'); + $bizParams = [ + 'functionTarget' => 'uni-id-co', + 'functionArgs' => [ + 'method' => 'login', + 'params' => [[ + 'password' => $this->password, + 'captcha' => '', + 'resetAppId' => '__UNI__unicloud_console', + 'resetUniPlatform' => 'web', + 'isReturnToken' => false, + 'email' => $this->username, + ]], + 'clientInfo' => $clientInfo, + ], + ]; + $params = [ + 'method' => 'serverless.function.runtime.invoke', + 'params' => json_encode($bizParams, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'spaceId' => 'uni-id-server', + 'timestamp' => getMillisecond(), + ]; + $sign = $this->sign($params, 'ba461799-fde8-429f-8cc4-4b6d306e2339'); + $post = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $headers = [ + 'Origin' => 'https://account.dcloud.net.cn', + 'Referer' => 'https://account.dcloud.net.cn/', + 'X-Client-Info' => json_encode($clientInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'X-Serverless-Sign' => $sign, + ]; + $response = http_request($url, $post, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['success']) && $result['success'] == true) { + if (isset($result['data']['errCode']) && $result['data']['errCode'] == 0) { + return $result['data']['newToken']['token']; + } else { + throw new Exception('登录失败:' . $result['data']['errMsg']); + } + } else { + throw new Exception('登录失败:' . $response['body']); + } + } + + private function getToken() + { + $uniIdToken = $this->login(); + $url = 'https://unicloud.dcloud.net.cn/client'; + $clientInfo = $this->getClientInfo('__UNI__unicloud_console', 'uniCloud控制台'); + $bizParams = [ + 'functionTarget' => 'uni-cloud-kernel', + 'functionArgs' => [ + 'action' => 'user/getUserToken', + 'data' => [ + 'isLogin' => true + ], + 'clientInfo' => $clientInfo, + 'uniIdToken' => $uniIdToken, + ], + ]; + $params = [ + 'method' => 'serverless.function.runtime.invoke', + 'params' => json_encode($bizParams, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'spaceId' => 'dc-6nfabcn6ada8d3dd', + 'timestamp' => getMillisecond(), + ]; + $sign = $this->sign($params, '4c1f7fbf-c732-42b0-ab10-4634a8bbe834'); + $post = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $headers = [ + 'Origin' => 'https://account.dcloud.net.cn', + 'Referer' => 'https://account.dcloud.net.cn/', + 'X-Client-Info' => json_encode($clientInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'X-Client-Token' => $uniIdToken, + 'X-Serverless-Sign' => $sign, + ]; + $response = http_request($url, $post, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['success']) && $result['success'] == true) { + if (isset($result['data']['code']) && $result['data']['code'] == 0) { + if (isset($result['data']['data']['ret']) && $result['data']['data']['ret'] == 0) { + $this->token = $result['data']['data']['data']['token']; + return $result['data']['data']['data']['token']; + } else { + throw new Exception('获取token失败:' . $result['data']['data']['desc']); + } + } else { + throw new Exception('获取token失败:' . $response['body']); + } + } else { + throw new Exception('获取token失败:' . $response['body']); + } + } + + private function getClientInfo($appId, $appName, $appVersion = '1.0.0', $appVersionCode = '100') + { + $clientInfo = [ + 'PLATFORM' => 'web', + 'OS' => 'windows', + 'APPID' => $appId, + 'DEVICEID' => $this->deviceId, + 'scene' => 1001, + 'appId' => $appId, + 'appLanguage' => 'zh-Hans', + 'appName' => $appName, + 'appVersion' => $appVersion, + 'appVersionCode' => $appVersionCode, + 'browserName' => 'chrome', + 'browserVersion' => '122.0.6261.95', + 'deviceId' => $this->deviceId, + 'deviceModel' => 'PC', + 'deviceType' => 'pc', + 'hostName' => 'chrome', + 'hostVersion' => '122.0.6261.95', + 'osName' => 'windows', + 'osVersion' => '10 x64', + 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36', + 'uniCompilerVersion' => '4.45', + 'uniPlatform' => 'web', + 'uniRuntimeVersion' => '4.45', + 'locale' => 'zh-Hans', + 'LOCALE' => 'zh-Hans', + ]; + return $clientInfo; + } + + private function sign($data, $key) + { + ksort($data); + $signstr = ''; + foreach ($data as $k => $v) { + $signstr .= $k . '=' . $v . '&'; + } + $signstr = rtrim($signstr, '&'); + return hash_hmac('md5', $signstr, $key); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/upyun.php b/app/lib/deploy/upyun.php index c6c684b..77ae4e9 100644 --- a/app/lib/deploy/upyun.php +++ b/app/lib/deploy/upyun.php @@ -1,133 +1,133 @@ -username = $config['username']; - $this->password = $config['password']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->username) || empty($this->password)) throw new Exception('用户名或密码不能为空'); - $this->login(); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $this->login(); - - $url = 'https://console.upyun.com/api/https/certificate/'; - $params = [ - 'certificate' => $fullchain, - 'private_key' => $privatekey, - ]; - $response = http_request($url, http_build_query($params), null, $this->cookie, null, $this->proxy); - $result = json_decode($response['body'], true); - if ($result['data']['status'] === 0) { - $common_name = $result['data']['result']['commonName']; - $certificate_id = $result['data']['result']['certificate_id']; - $this->log('证书上传成功!证书ID:' . $certificate_id); - } elseif (isset($result['data']['message'])) { - throw new Exception('证书上传失败:' . $result['data']['message']); - } else { - throw new Exception('证书上传失败'); - } - - $url = 'https://console.upyun.com/api/https/certificate/search'; - $params = [ - 'limit' => 100, - 'domain' => $common_name, - ]; - $response = http_request($url . '?' . http_build_query($params), null, null, $this->cookie, null, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['data']['result']) && is_array($result['data']['result'])) { - $cert_list = $result['data']['result']; - } elseif (isset($result['data']['message'])) { - throw new Exception('查找证书失败:' . $result['data']['message']); - } else { - throw new Exception('查找证书失败'); - } - - $i = 0; - $d = 0; - foreach ($cert_list as $crt_id => $item) { - if ($crt_id == $certificate_id || $item['commonName'] != $common_name || $item['config_domain'] == 0) { - continue; - } - $url = 'https://console.upyun.com/api/https/migrate/certificate'; - $params = [ - 'new_crt_id' => $certificate_id, - 'old_crt_id' => $crt_id, - ]; - $response = http_request($url, http_build_query($params), null, $this->cookie, null, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['data']['result']) && $result['data']['result'] == true) { - $i++; - $d += $item['config_domain']; - $this->log('证书ID:' . $crt_id . ' 迁移成功!'); - } elseif (isset($result['data']['message'])) { - throw new Exception('证书迁移失败:' . $result['data']['message']); - } else { - throw new Exception('证书迁移失败'); - } - } - - if ($i == 0) throw new Exception('未找到可迁移的证书'); - $this->log('共迁移' . $i . '个证书,关联域名' . $d . '个'); - } - - private function login() - { - $url = 'https://console.upyun.com/accounts/signin/'; - $params = [ - 'username' => $this->username, - 'password' => $this->password, - ]; - $response = http_request($url, http_build_query($params), null, null, null, $this->proxy); - $result = json_decode($response['body'], true); - if (isset($result['data']['result']) && $result['data']['result'] == true) { - $cookie = ''; - if (isset($response['headers']['set-cookie'])) { - foreach ($response['headers']['set-cookie'] as $val) { - $arr = explode('=', $val); - if ($arr[1] == '' || $arr[1] == 'deleted') continue; - $cookie .= $val . '; '; - } - } else { - throw new Exception('登录成功,获取cookie失败'); - } - $this->cookie = $cookie; - return true; - } elseif (isset($result['data']['message'])) { - throw new Exception('登录失败:' . $result['data']['message']); - } else { - throw new Exception('登录失败'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +username = $config['username']; + $this->password = $config['password']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->username) || empty($this->password)) throw new Exception('用户名或密码不能为空'); + $this->login(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $this->login(); + + $url = 'https://console.upyun.com/api/https/certificate/'; + $params = [ + 'certificate' => $fullchain, + 'private_key' => $privatekey, + ]; + $response = http_request($url, http_build_query($params), null, $this->cookie, null, $this->proxy); + $result = json_decode($response['body'], true); + if ($result['data']['status'] === 0) { + $common_name = $result['data']['result']['commonName']; + $certificate_id = $result['data']['result']['certificate_id']; + $this->log('证书上传成功!证书ID:' . $certificate_id); + } elseif (isset($result['data']['message'])) { + throw new Exception('证书上传失败:' . $result['data']['message']); + } else { + throw new Exception('证书上传失败'); + } + + $url = 'https://console.upyun.com/api/https/certificate/search'; + $params = [ + 'limit' => 100, + 'domain' => $common_name, + ]; + $response = http_request($url . '?' . http_build_query($params), null, null, $this->cookie, null, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['data']['result']) && is_array($result['data']['result'])) { + $cert_list = $result['data']['result']; + } elseif (isset($result['data']['message'])) { + throw new Exception('查找证书失败:' . $result['data']['message']); + } else { + throw new Exception('查找证书失败'); + } + + $i = 0; + $d = 0; + foreach ($cert_list as $crt_id => $item) { + if ($crt_id == $certificate_id || $item['commonName'] != $common_name || $item['config_domain'] == 0) { + continue; + } + $url = 'https://console.upyun.com/api/https/migrate/certificate'; + $params = [ + 'new_crt_id' => $certificate_id, + 'old_crt_id' => $crt_id, + ]; + $response = http_request($url, http_build_query($params), null, $this->cookie, null, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['data']['result']) && $result['data']['result'] == true) { + $i++; + $d += $item['config_domain']; + $this->log('证书ID:' . $crt_id . ' 迁移成功!'); + } elseif (isset($result['data']['message'])) { + throw new Exception('证书迁移失败:' . $result['data']['message']); + } else { + throw new Exception('证书迁移失败'); + } + } + + if ($i == 0) throw new Exception('未找到可迁移的证书'); + $this->log('共迁移' . $i . '个证书,关联域名' . $d . '个'); + } + + private function login() + { + $url = 'https://console.upyun.com/accounts/signin/'; + $params = [ + 'username' => $this->username, + 'password' => $this->password, + ]; + $response = http_request($url, http_build_query($params), null, null, null, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['data']['result']) && $result['data']['result'] == true) { + $cookie = ''; + if (isset($response['headers']['set-cookie'])) { + foreach ($response['headers']['set-cookie'] as $val) { + $arr = explode('=', $val); + if ($arr[1] == '' || $arr[1] == 'deleted') continue; + $cookie .= $val . '; '; + } + } else { + throw new Exception('登录成功,获取cookie失败'); + } + $this->cookie = $cookie; + return true; + } elseif (isset($result['data']['message'])) { + throw new Exception('登录失败:' . $result['data']['message']); + } else { + throw new Exception('登录失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/uusec.php b/app/lib/deploy/uusec.php index f578643..e651411 100644 --- a/app/lib/deploy/uusec.php +++ b/app/lib/deploy/uusec.php @@ -1,103 +1,103 @@ -url = rtrim($config['url'], '/'); - $this->username = $config['username']; - $this->password = $config['password']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->password) || empty($this->password)) throw new Exception('用户名和密码不能为空'); - $this->login(); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $id = $config['id']; - if (empty($id)) throw new Exception('证书ID不能为空'); - - $this->login(); - - $params = [ - 'id' => intval($id), - 'type' => 1, - 'name' => $config['name'], - 'crt' => $fullchain, - 'key' => $privatekey, - ]; - $result = $this->request('/api/v1/certs', $params, 'PUT'); - if (is_string($result) && $result == 'OK') { - $this->log('证书ID:' . $id . '更新成功!'); - } else { - throw new Exception('证书ID:' . $id . '更新失败,' . (isset($result['err']) ? $result['err'] : '未知错误')); - } - } - - private function login() - { - $path = '/api/v1/users/login'; - $params = [ - 'usr' => $this->username, - 'pwd' => $this->password, - 'otp' => '', - ]; - $result = $this->request($path, $params); - if (isset($result['token'])) { - $this->accessToken = $result['token']; - } else { - throw new Exception('登录失败,' . (isset($result['err']) ? $result['err'] : '未知错误')); - } - } - - private function request($path, $params = null, $method = null) - { - $url = $this->url . $path; - $headers = []; - $body = null; - if ($this->accessToken) { - $headers['Authorization'] = 'Bearer ' . $this->accessToken; - } - if ($params) { - $headers['Content-Type'] = 'application/json;charset=UTF-8'; - $body = json_encode($params); - } - $response = http_request($url, $body, null, null, $headers, $this->proxy, $method); - $result = json_decode($response['body'], true); - if ($response['code'] == 200) { - return $result; - } elseif (isset($result['message'])) { - throw new Exception($result['message']); - } else { - throw new Exception('请求失败,HTTP状态码:' . $response['code']); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +url = rtrim($config['url'], '/'); + $this->username = $config['username']; + $this->password = $config['password']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->password) || empty($this->password)) throw new Exception('用户名和密码不能为空'); + $this->login(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $id = $config['id']; + if (empty($id)) throw new Exception('证书ID不能为空'); + + $this->login(); + + $params = [ + 'id' => intval($id), + 'type' => 1, + 'name' => $config['name'], + 'crt' => $fullchain, + 'key' => $privatekey, + ]; + $result = $this->request('/api/v1/certs', $params, 'PUT'); + if (is_string($result) && $result == 'OK') { + $this->log('证书ID:' . $id . '更新成功!'); + } else { + throw new Exception('证书ID:' . $id . '更新失败,' . (isset($result['err']) ? $result['err'] : '未知错误')); + } + } + + private function login() + { + $path = '/api/v1/users/login'; + $params = [ + 'usr' => $this->username, + 'pwd' => $this->password, + 'otp' => '', + ]; + $result = $this->request($path, $params); + if (isset($result['token'])) { + $this->accessToken = $result['token']; + } else { + throw new Exception('登录失败,' . (isset($result['err']) ? $result['err'] : '未知错误')); + } + } + + private function request($path, $params = null, $method = null) + { + $url = $this->url . $path; + $headers = []; + $body = null; + if ($this->accessToken) { + $headers['Authorization'] = 'Bearer ' . $this->accessToken; + } + if ($params) { + $headers['Content-Type'] = 'application/json;charset=UTF-8'; + $body = json_encode($params); + } + $response = http_request($url, $body, null, null, $headers, $this->proxy, $method); + $result = json_decode($response['body'], true); + if ($response['code'] == 200) { + return $result; + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + throw new Exception('请求失败,HTTP状态码:' . $response['code']); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/west.php b/app/lib/deploy/west.php index ac357d0..9fb9c9e 100644 --- a/app/lib/deploy/west.php +++ b/app/lib/deploy/west.php @@ -1,131 +1,131 @@ -username = $config['username']; - $this->api_password = $config['api_password']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->username) || empty($this->api_password)) throw new Exception('用户名或API密码不能为空'); - $this->execute('/vhost/', ['act' => 'products']); - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - if (empty($config['sitename'])) throw new Exception('FTP账号不能为空'); - $params = [ - 'act' => 'vhostssl', - 'sitename' => $config['sitename'], - 'cmd' => 'info' - ]; - try { - $data = $this->execute('/vhost/', $params); - } catch (Exception $e) { - throw new Exception('获取虚拟主机SSL配置失败:' . $e->getMessage()); - } - - $params = [ - 'act' => 'vhostssl', - 'sitename' => $config['sitename'], - 'cmd' => 'import', - 'keycontent' => $privatekey, - 'certcontent' => $fullchain, - ]; - try { - $this->execute('/vhost/', $params); - } catch (Exception $e) { - throw new Exception('上传SSL证书失败:' . $e->getMessage()); - } - $this->log('SSL证书上传成功'); - - if (!isset($data['SSLEnabled']) || $data['SSLEnabled'] == 0) { - $params = [ - 'act' => 'vhostssl', - 'sitename' => $config['sitename'], - 'cmd' => 'openssl', - ]; - try { - $this->execute('/vhost/', $params); - } catch (Exception $e) { - throw new Exception('虚拟主机部署SSL失败:' . $e->getMessage()); - } - } else { - $params = [ - 'act' => 'vhostssl', - 'sitename' => $config['sitename'], - 'cmd' => 'info' - ]; - try { - $data = $this->execute('/vhost/', $params); - } catch (Exception $e) { - throw new Exception('获取虚拟主机SSL配置失败:' . $e->getMessage()); - } - if (!empty($data['sslcert']['ssl'])) { - foreach ($data['sslcert']['ssl'] as $domain => $row) { - if (!in_array($domain, $config['domainList'])) continue; - $params = [ - 'act' => 'vhostssl', - 'sitename' => $config['sitename'], - 'cmd' => 'clearsslcache', - 'sslid' => $row['sysid'], - 'dm' => $domain, - ]; - try { - $this->execute('/vhost/', $params); - $this->log('更新' . $domain . '证书缓存成功'); - } catch (Exception $e) { - $this->log('更新' . $domain . '证书缓存失败:' . $e->getMessage()); - } - } - } - } - $this->log('虚拟主机' . $config['sitename'] . '部署SSL成功'); - } - - private function execute($path, $params) - { - $params['username'] = $this->username; - $params['time'] = getMillisecond(); - $params['token'] = md5($this->username . $this->api_password . $params['time']); - $response = http_request($this->baseUrl . $path, str_replace('+', '%20', http_build_query($params)), null, null, null, $this->proxy); - $response = mb_convert_encoding($response['body'], 'UTF-8', 'GBK'); - $arr = json_decode($response, true); - if ($arr) { - if ($arr['result'] == 200) { - return isset($arr['data']) ? $arr['data'] : []; - } else { - throw new Exception($arr['msg']); - } - } else { - throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } -} +username = $config['username']; + $this->api_password = $config['api_password']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->username) || empty($this->api_password)) throw new Exception('用户名或API密码不能为空'); + $this->execute('/vhost/', ['act' => 'products']); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['sitename'])) throw new Exception('FTP账号不能为空'); + $params = [ + 'act' => 'vhostssl', + 'sitename' => $config['sitename'], + 'cmd' => 'info' + ]; + try { + $data = $this->execute('/vhost/', $params); + } catch (Exception $e) { + throw new Exception('获取虚拟主机SSL配置失败:' . $e->getMessage()); + } + + $params = [ + 'act' => 'vhostssl', + 'sitename' => $config['sitename'], + 'cmd' => 'import', + 'keycontent' => $privatekey, + 'certcontent' => $fullchain, + ]; + try { + $this->execute('/vhost/', $params); + } catch (Exception $e) { + throw new Exception('上传SSL证书失败:' . $e->getMessage()); + } + $this->log('SSL证书上传成功'); + + if (!isset($data['SSLEnabled']) || $data['SSLEnabled'] == 0) { + $params = [ + 'act' => 'vhostssl', + 'sitename' => $config['sitename'], + 'cmd' => 'openssl', + ]; + try { + $this->execute('/vhost/', $params); + } catch (Exception $e) { + throw new Exception('虚拟主机部署SSL失败:' . $e->getMessage()); + } + } else { + $params = [ + 'act' => 'vhostssl', + 'sitename' => $config['sitename'], + 'cmd' => 'info' + ]; + try { + $data = $this->execute('/vhost/', $params); + } catch (Exception $e) { + throw new Exception('获取虚拟主机SSL配置失败:' . $e->getMessage()); + } + if (!empty($data['sslcert']['ssl'])) { + foreach ($data['sslcert']['ssl'] as $domain => $row) { + if (!in_array($domain, $config['domainList'])) continue; + $params = [ + 'act' => 'vhostssl', + 'sitename' => $config['sitename'], + 'cmd' => 'clearsslcache', + 'sslid' => $row['sysid'], + 'dm' => $domain, + ]; + try { + $this->execute('/vhost/', $params); + $this->log('更新' . $domain . '证书缓存成功'); + } catch (Exception $e) { + $this->log('更新' . $domain . '证书缓存失败:' . $e->getMessage()); + } + } + } + } + $this->log('虚拟主机' . $config['sitename'] . '部署SSL成功'); + } + + private function execute($path, $params) + { + $params['username'] = $this->username; + $params['time'] = getMillisecond(); + $params['token'] = md5($this->username . $this->api_password . $params['time']); + $response = http_request($this->baseUrl . $path, str_replace('+', '%20', http_build_query($params)), null, null, null, $this->proxy); + $response = mb_convert_encoding($response['body'], 'UTF-8', 'GBK'); + $arr = json_decode($response, true); + if ($arr) { + if ($arr['result'] == 200) { + return isset($arr['data']) ? $arr['data'] : []; + } else { + throw new Exception($arr['msg']); + } + } else { + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/xp.php b/app/lib/deploy/xp.php index 6d27201..0657010 100644 --- a/app/lib/deploy/xp.php +++ b/app/lib/deploy/xp.php @@ -1,113 +1,113 @@ -url = rtrim($config['url'], '/'); - $this->apikey = $config['apikey']; - $this->proxy = $config['proxy'] == 1; - } - - public function check() - { - if (empty($this->url) || empty($this->apikey)) throw new Exception('请填写面板地址和接口密钥'); - - $path = '/openApi/siteList'; - $response = $this->request($path); - $result = json_decode($response, true); - if (isset($result['code']) && $result['code'] == 1000) { - return true; - } else { - throw new Exception(isset($result['message']) ? $result['message'] : '面板地址无法连接'); - } - } - - public function deploy($fullchain, $privatekey, $config, &$info) - { - $path = '/openApi/siteList'; - $response = $this->request($path); - $result = json_decode($response, true); - if (isset($result['code']) && $result['code'] == 1000) { - - $sites = explode("\n", $config['sites']); - $sites = array_map('trim', $sites); - $success = 0; - $errmsg = null; - - foreach ($result['data'] as $item) { - if (!in_array($item['name'], $sites)) { - continue; - } - try { - $this->deploySite($item['id'], $fullchain, $privatekey); - $this->log("网站 {$item['name']} 证书部署成功"); - $success++; - } catch (Exception $e) { - $errmsg = $e->getMessage(); - $this->log("网站 {$item['name']} 证书部署失败:" . $errmsg); - } - } - if ($success == 0) { - throw new Exception($errmsg ? $errmsg : '要部署的网站不存在'); - } - - } elseif (isset($result['message'])) { - throw new Exception($result['message']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } - - private function deploySite($id, $fullchain, $privatekey) - { - $path = '/openApi/setSSL'; - $data = [ - 'id' => $id, - 'key' => $privatekey, - 'pem' => $fullchain, - ]; - $response = $this->request($path, $data); - $result = json_decode($response, true); - if (isset($result['code']) && $result['code'] == 1000) { - return true; - } elseif (isset($result['message'])) { - throw new Exception($result['message']); - } else { - throw new Exception($response ? $response : '返回数据解析失败'); - } - } - - public function setLogger($func) - { - $this->logger = $func; - } - - private function log($txt) - { - if ($this->logger) { - call_user_func($this->logger, $txt); - } - } - - private function request($path, $params = null) - { - $url = $this->url . $path; - - $headers = [ - 'XP-API-KEY' => $this->apikey, - ]; - $response = http_request($url, $params ? json_encode($params) : null, null, null, $headers, $this->proxy); - return $response['body']; - } -} +url = rtrim($config['url'], '/'); + $this->apikey = $config['apikey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->apikey)) throw new Exception('请填写面板地址和接口密钥'); + + $path = '/openApi/siteList'; + $response = $this->request($path); + $result = json_decode($response, true); + if (isset($result['code']) && $result['code'] == 1000) { + return true; + } else { + throw new Exception(isset($result['message']) ? $result['message'] : '面板地址无法连接'); + } + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $path = '/openApi/siteList'; + $response = $this->request($path); + $result = json_decode($response, true); + if (isset($result['code']) && $result['code'] == 1000) { + + $sites = explode("\n", $config['sites']); + $sites = array_map('trim', $sites); + $success = 0; + $errmsg = null; + + foreach ($result['data'] as $item) { + if (!in_array($item['name'], $sites)) { + continue; + } + try { + $this->deploySite($item['id'], $fullchain, $privatekey); + $this->log("网站 {$item['name']} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("网站 {$item['name']} 证书部署失败:" . $errmsg); + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '要部署的网站不存在'); + } + + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + + private function deploySite($id, $fullchain, $privatekey) + { + $path = '/openApi/setSSL'; + $data = [ + 'id' => $id, + 'key' => $privatekey, + 'pem' => $fullchain, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['code']) && $result['code'] == 1000) { + return true; + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($path, $params = null) + { + $url = $this->url . $path; + + $headers = [ + 'XP-API-KEY' => $this->apikey, + ]; + $response = http_request($url, $params ? json_encode($params) : null, null, null, $headers, $this->proxy); + return $response['body']; + } +} diff --git a/app/lib/dns/baidu.php b/app/lib/dns/baidu.php index 34e29bc..ad91c53 100644 --- a/app/lib/dns/baidu.php +++ b/app/lib/dns/baidu.php @@ -1,258 +1,258 @@ -AccessKeyId = $config['ak']; - $this->SecretAccessKey = $config['sk']; - $proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - $this->client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $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) - { - $query = ['name' => $KeyWord]; - $data = $this->send_reuqest('GET', '/v1/dns/zone', $query); - if ($data) { - $list = []; - foreach ($data['zones'] as $row) { - $list[] = [ - 'DomainId' => $row['id'], - 'Domain' => rtrim($row['name'], '.'), - 'RecordCount' => 0, - ]; - } - return ['total' => count($list), 'list' => $list]; - } - return false; - } - - //获取解析记录列表 - public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null) - { - $query = []; - if (!isNullOrEmpty($SubDomain)) { - $SubDomain = strtolower($SubDomain); - $query['rr'] = $SubDomain; - } - $data = $this->send_reuqest('GET', '/v1/dns/zone/'.$this->domain.'/record', $query); - if ($data) { - $list = []; - foreach ($data['records'] as $row) { - $list[] = [ - 'RecordId' => $row['id'], - 'Domain' => $this->domain, - 'Name' => $row['rr'], - 'Type' => $row['type'], - 'Value' => $row['value'], - 'Line' => $row['line'], - 'TTL' => $row['ttl'], - 'MX' => $row['priority'], - 'Status' => $row['status'] == 'running' ? '1' : '0', - 'Weight' => null, - 'Remark' => $row['description'], - 'UpdateTime' => null, - ]; - } - if (!isNullOrEmpty($SubDomain)) { - $list = array_values(array_filter($list, function ($v) use ($SubDomain) { - return $v['Name'] == $SubDomain; - })); - } else { - if (!isNullOrEmpty($KeyWord)) { - $list = array_values(array_filter($list, function ($v) use ($KeyWord) { - return strpos($v['Name'], $KeyWord) !== false || strpos($v['Value'], $KeyWord) !== false; - })); - } - if (!isNullOrEmpty($Value)) { - $list = array_values(array_filter($list, function ($v) use ($Value) { - return $v['Value'] == $Value; - })); - } - if (!isNullOrEmpty($Type)) { - $list = array_values(array_filter($list, function ($v) use ($Type) { - return $v['Type'] == $Type; - })); - } - if (!isNullOrEmpty($Status)) { - $list = array_values(array_filter($list, function ($v) use ($Status) { - return $v['Status'] == $Status; - })); - } - } - return ['total' => count($list), 'list' => $list]; - } - return false; - } - - //获取子域名解析记录列表 - public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null) - { - if ($SubDomain == '') $SubDomain = '@'; - return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line); - } - - //获取解析记录详细信息 - public function getDomainRecordInfo($RecordId) - { - $query = ['id' => $RecordId]; - $data = $this->send_reuqest('GET', '/v1/dns/zone/'.$this->domain.'/record', $query); - if ($data && !empty($data['records'])) { - $data = $data['records'][0]; - return [ - 'RecordId' => $data['id'], - 'Domain' => rtrim($data['zone_name'], '.'), - 'Name' => str_replace('.'.$data['zone_name'], '', $data['name']), - 'Type' => $data['type'], - 'Value' => $data['value'], - 'Line' => $data['line'], - 'TTL' => $data['ttl'], - 'MX' => $data['priority'], - 'Status' => $data['status'] == 'running' ? '1' : '0', - 'Weight' => null, - 'Remark' => $data['description'], - 'UpdateTime' => null, - ]; - } - return false; - } - - //添加解析记录 - public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $params = ['rr' => $Name, 'type' => $this->convertType($Type), 'value' => $Value, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark]; - if ($Type == 'MX') $params['priority'] = intval($MX); - $query = ['clientToken' => getSid()]; - return $this->send_reuqest('POST', '/v1/dns/zone/'.$this->domain.'/record', $query, $params); - } - - //修改解析记录 - public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $params = ['rr' => $Name, 'type' => $this->convertType($Type), 'value' => $Value, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark]; - if ($Type == 'MX') $params['priority'] = intval($MX); - $query = ['clientToken' => getSid()]; - return $this->send_reuqest('PUT', '/v1/dns/zone/'.$this->domain.'/record/'.$RecordId, $query, $params); - } - - //修改解析记录备注 - public function updateDomainRecordRemark($RecordId, $Remark) - { - return false; - } - - //删除解析记录 - public function deleteDomainRecord($RecordId) - { - $query = ['clientToken' => getSid()]; - return $this->send_reuqest('DELETE', '/v1/dns/zone/'.$this->domain.'/record/'.$RecordId, $query); - } - - //设置解析记录状态 - public function setDomainRecordStatus($RecordId, $Status) - { - $Status = $Status == '1' ? 'enable' : 'disable'; - $query = [$Status => '', 'clientToken' => getSid()]; - return $this->send_reuqest('PUT', '/v1/dns/zone/'.$this->domain.'/record/'.$RecordId, $query); - } - - //获取解析记录操作日志 - public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) - { - return false; - } - - //获取解析线路列表 - public function getRecordLine() - { - return [ - 'default' => ['name' => '默认', 'parent' => null], - 'ct' => ['name' => '电信', 'parent' => null], - 'cnc' => ['name' => '联通', 'parent' => null], - 'cmnet' => ['name' => '移动', 'parent' => null], - 'edu' => ['name' => '教育网', 'parent' => null], - 'search' => ['name' => '搜索引擎(百度)', 'parent' => null], - ]; - } - - //获取域名概览信息 - public function getDomainInfo() - { - $res = $this->getDomainList($this->domain); - if ($res && !empty($res['list'])) { - return $res['list'][0]; - } - return false; - } - - //获取域名最低TTL - public function getMinTTL() - { - return false; - } - - public function addDomain($Domain) - { - $query = ['clientToken' => getSid(), 'name' => $Domain]; - $res = $this->send_reuqest('POST', '/v1/dns/zone', null, $query); - if ($res) { - $data = $this->getDomainInfo($Domain); - if ($data) { - return ['id' => $data['DomainId'], 'name' => $data['Domain']]; - } - } - return false; - } - - private function convertType($type) - { - return $type; - } - - private function send_reuqest($method, $path, $query = null, $params = null) - { - try{ - return $this->client->request($method, $path, $query, $params); - }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); - } -} +AccessKeyId = $config['ak']; + $this->SecretAccessKey = $config['sk']; + $proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + $this->client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $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) + { + $query = ['name' => $KeyWord]; + $data = $this->send_reuqest('GET', '/v1/dns/zone', $query); + if ($data) { + $list = []; + foreach ($data['zones'] as $row) { + $list[] = [ + 'DomainId' => $row['id'], + 'Domain' => rtrim($row['name'], '.'), + 'RecordCount' => 0, + ]; + } + return ['total' => count($list), 'list' => $list]; + } + return false; + } + + //获取解析记录列表 + public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null) + { + $query = []; + if (!isNullOrEmpty($SubDomain)) { + $SubDomain = strtolower($SubDomain); + $query['rr'] = $SubDomain; + } + $data = $this->send_reuqest('GET', '/v1/dns/zone/'.$this->domain.'/record', $query); + if ($data) { + $list = []; + foreach ($data['records'] as $row) { + $list[] = [ + 'RecordId' => $row['id'], + 'Domain' => $this->domain, + 'Name' => $row['rr'], + 'Type' => $row['type'], + 'Value' => $row['value'], + 'Line' => $row['line'], + 'TTL' => $row['ttl'], + 'MX' => $row['priority'], + 'Status' => $row['status'] == 'running' ? '1' : '0', + 'Weight' => null, + 'Remark' => $row['description'], + 'UpdateTime' => null, + ]; + } + if (!isNullOrEmpty($SubDomain)) { + $list = array_values(array_filter($list, function ($v) use ($SubDomain) { + return $v['Name'] == $SubDomain; + })); + } else { + if (!isNullOrEmpty($KeyWord)) { + $list = array_values(array_filter($list, function ($v) use ($KeyWord) { + return strpos($v['Name'], $KeyWord) !== false || strpos($v['Value'], $KeyWord) !== false; + })); + } + if (!isNullOrEmpty($Value)) { + $list = array_values(array_filter($list, function ($v) use ($Value) { + return $v['Value'] == $Value; + })); + } + if (!isNullOrEmpty($Type)) { + $list = array_values(array_filter($list, function ($v) use ($Type) { + return $v['Type'] == $Type; + })); + } + if (!isNullOrEmpty($Status)) { + $list = array_values(array_filter($list, function ($v) use ($Status) { + return $v['Status'] == $Status; + })); + } + } + return ['total' => count($list), 'list' => $list]; + } + return false; + } + + //获取子域名解析记录列表 + public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null) + { + if ($SubDomain == '') $SubDomain = '@'; + return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line); + } + + //获取解析记录详细信息 + public function getDomainRecordInfo($RecordId) + { + $query = ['id' => $RecordId]; + $data = $this->send_reuqest('GET', '/v1/dns/zone/'.$this->domain.'/record', $query); + if ($data && !empty($data['records'])) { + $data = $data['records'][0]; + return [ + 'RecordId' => $data['id'], + 'Domain' => rtrim($data['zone_name'], '.'), + 'Name' => str_replace('.'.$data['zone_name'], '', $data['name']), + 'Type' => $data['type'], + 'Value' => $data['value'], + 'Line' => $data['line'], + 'TTL' => $data['ttl'], + 'MX' => $data['priority'], + 'Status' => $data['status'] == 'running' ? '1' : '0', + 'Weight' => null, + 'Remark' => $data['description'], + 'UpdateTime' => null, + ]; + } + return false; + } + + //添加解析记录 + public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $params = ['rr' => $Name, 'type' => $this->convertType($Type), 'value' => $Value, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark]; + if ($Type == 'MX') $params['priority'] = intval($MX); + $query = ['clientToken' => getSid()]; + return $this->send_reuqest('POST', '/v1/dns/zone/'.$this->domain.'/record', $query, $params); + } + + //修改解析记录 + public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $params = ['rr' => $Name, 'type' => $this->convertType($Type), 'value' => $Value, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark]; + if ($Type == 'MX') $params['priority'] = intval($MX); + $query = ['clientToken' => getSid()]; + return $this->send_reuqest('PUT', '/v1/dns/zone/'.$this->domain.'/record/'.$RecordId, $query, $params); + } + + //修改解析记录备注 + public function updateDomainRecordRemark($RecordId, $Remark) + { + return false; + } + + //删除解析记录 + public function deleteDomainRecord($RecordId) + { + $query = ['clientToken' => getSid()]; + return $this->send_reuqest('DELETE', '/v1/dns/zone/'.$this->domain.'/record/'.$RecordId, $query); + } + + //设置解析记录状态 + public function setDomainRecordStatus($RecordId, $Status) + { + $Status = $Status == '1' ? 'enable' : 'disable'; + $query = [$Status => '', 'clientToken' => getSid()]; + return $this->send_reuqest('PUT', '/v1/dns/zone/'.$this->domain.'/record/'.$RecordId, $query); + } + + //获取解析记录操作日志 + public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) + { + return false; + } + + //获取解析线路列表 + public function getRecordLine() + { + return [ + 'default' => ['name' => '默认', 'parent' => null], + 'ct' => ['name' => '电信', 'parent' => null], + 'cnc' => ['name' => '联通', 'parent' => null], + 'cmnet' => ['name' => '移动', 'parent' => null], + 'edu' => ['name' => '教育网', 'parent' => null], + 'search' => ['name' => '搜索引擎(百度)', 'parent' => null], + ]; + } + + //获取域名概览信息 + public function getDomainInfo() + { + $res = $this->getDomainList($this->domain); + if ($res && !empty($res['list'])) { + return $res['list'][0]; + } + return false; + } + + //获取域名最低TTL + public function getMinTTL() + { + return false; + } + + public function addDomain($Domain) + { + $query = ['clientToken' => getSid(), 'name' => $Domain]; + $res = $this->send_reuqest('POST', '/v1/dns/zone', null, $query); + if ($res) { + $data = $this->getDomainInfo($Domain); + if ($data) { + return ['id' => $data['DomainId'], 'name' => $data['Domain']]; + } + } + return false; + } + + private function convertType($type) + { + return $type; + } + + private function send_reuqest($method, $path, $query = null, $params = null) + { + try{ + return $this->client->request($method, $path, $query, $params); + }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); + } +} diff --git a/app/lib/dns/cloudflare.php b/app/lib/dns/cloudflare.php index 30456de..63681ba 100644 --- a/app/lib/dns/cloudflare.php +++ b/app/lib/dns/cloudflare.php @@ -1,329 +1,329 @@ -Email = $config['ak']; - $this->ApiKey = $config['sk']; - $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) - { - $param = ['page' => $PageNumber, 'per_page' => $PageSize]; - if (!empty($KeyWord)) { - $param['name'] = $KeyWord; - } - $data = $this->send_reuqest('GET', '/zones', $param); - if ($data) { - $list = []; - foreach ($data['result'] as $row) { - $list[] = [ - 'DomainId' => $row['id'], - 'Domain' => $row['name'], - 'RecordCount' => 0, - ]; - } - return ['total' => $data['result_info']['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 (!isNullOrEmpty($Value)) $KeyWord = $Value; - $param = ['type' => $Type, 'search' => $KeyWord, 'page' => $PageNumber, 'per_page' => $PageSize]; - if (!isNullOrEmpty($SubDomain)) { - if ($SubDomain == '@') $SubDomain = $this->domain; - else $SubDomain .= '.' . $this->domain; - $param['name'] = $SubDomain; - } - if (!isNullOrEmpty($Line)) { - $param['proxied'] = $Line == '1' ? 'true' : 'false'; - } - $data = $this->send_reuqest('GET', '/zones/'.$this->domainid.'/dns_records', $param); - if ($data) { - $list = []; - foreach ($data['result'] as $row) { - $name = $this->domain == $row['name'] ? '@' : str_replace('.'.$this->domain, '', $row['name']); - $status = str_ends_with($name, '_pause') ? '0' : '1'; - $name = $status == '0' ? substr($name, 0, -6) : $name; - $list[] = [ - 'RecordId' => $row['id'], - 'Domain' => $this->domain, - 'Name' => $name, - 'Type' => $row['type'], - 'Value' => $row['content'], - 'Line' => $row['proxied'] ? '1' : '0', - 'TTL' => $row['ttl'], - 'MX' => isset($row['priority']) ? $row['priority'] : null, - 'Status' => $status, - 'Weight' => null, - 'Remark' => $row['comment'], - 'UpdateTime' => $row['modified_on'], - ]; - } - return ['total' => $data['result_info']['total_count'], '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) - { - $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']); - $status = str_ends_with($name, '_pause') ? '0' : '1'; - $name = $status == '0' ? substr($name, 0, -6) : $name; - return [ - 'RecordId' => $data['result']['id'], - 'Domain' => $this->domain, - 'Name' => $name, - 'Type' => $data['result']['type'], - 'Value' => $data['result']['content'], - 'Line' => $data['result']['proxied'] ? '1' : '0', - 'TTL' => $data['result']['ttl'], - 'MX' => isset($data['result']['priority']) ? $data['result']['priority'] : null, - 'Status' => $status, - 'Weight' => null, - 'Remark' => $data['result']['comment'], - 'UpdateTime' => $data['result']['modified_on'], - ]; - } - return false; - } - - //添加解析记录 - public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $param = ['name' => $Name, 'type' => $this->convertType($Type), 'content' => $Value, 'proxied' => $Line == '1', 'ttl' => intval($TTL), 'comment' => $Remark]; - if ($Type == 'MX') $param['priority'] = intval($MX); - if ($Type == 'CAA' || $Type == 'SRV') { - unset($param['content']); - $param['data'] = $this->convertValue($Value, $Type); - } - $data = $this->send_reuqest('POST', '/zones/'.$this->domainid.'/dns_records', $param); - return is_array($data) ? $data['result']['id'] : false; - } - - //修改解析记录 - public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $param = ['name' => $Name, 'type' => $this->convertType($Type), 'content' => $Value, 'proxied' => $Line == '1', 'ttl' => intval($TTL), 'comment' => $Remark]; - if ($Type == 'MX') $param['priority'] = intval($MX); - if ($Type == 'CAA' || $Type == 'SRV') { - unset($param['content']); - $param['data'] = $this->convertValue($Value, $Type); - } - $data = $this->send_reuqest('PATCH', '/zones/'.$this->domainid.'/dns_records/'.$RecordId, $param); - return is_array($data); - } - - //修改解析记录备注 - public function updateDomainRecordRemark($RecordId, $Remark) - { - return false; - } - - //删除解析记录 - public function deleteDomainRecord($RecordId) - { - $data = $this->send_reuqest('DELETE', '/zones/'.$this->domainid.'/dns_records/'.$RecordId); - return is_array($data); - } - - //设置解析记录状态 - public function setDomainRecordStatus($RecordId, $Status) - { - $info = $this->getDomainRecordInfo($RecordId); - $Name = $Status == '1' ? str_replace('_pause', '', $info['Name']) : $info['Name'] . '_pause'; - return $this->updateDomainRecord($RecordId, $Name, $info['Type'], $info['Value'], $info['Line'], $info['TTL'], $info['MX'], $info['Weight'], $info['Remark']); - } - - //获取解析记录操作日志 - 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() - { - $data = $this->send_reuqest('GET', '/zones/'.$this->domainid); - if ($data) { - return $data['result']; - } - return false; - } - - //获取域名最低TTL - public function getMinTTL() - { - return false; - } - - public function addDomain($Domain) - { - $param = ['name' => $Domain]; - $data = $this->send_reuqest('POST', '/zones', $param); - if ($data) { - return ['id' => $data['result']['id'], 'name' => $data['result']['name']]; - } - return false; - } - - private function convertType($type) - { - $convert_dict = ['REDIRECT_URL' => 'URI', 'FORWARD_URL' => 'URI']; - if (array_key_exists($type, $convert_dict)) { - return $convert_dict[$type]; - } - return $type; - } - - private function convertValue($value, $type) - { - if ($type == 'SRV') { - $arr = explode(' ', $value); - if (count($arr) > 3) { - $data = [ - 'priority' => intval($arr[0]), - 'weight' => intval($arr[1]), - 'port' => intval($arr[2]), - 'target' => $arr[3], - ]; - } else { - $data = [ - 'weight' => intval($arr[0]), - 'port' => intval($arr[1]), - 'target' => $arr[2], - ]; - } - } elseif ($type == 'CAA') { - $arr = explode(' ', $value); - $data = [ - 'flags' => intval($arr[0]), - 'tag' => $arr[1], - 'value' => trim($arr[2], '"'), - ]; - } - return $data; - } - - private function send_reuqest($method, $path, $params = null) - { - $url = $this->baseUrl . $path; - - if (preg_match('/^[0-9a-z]+$/i', $this->ApiKey)) { - $headers = [ - 'X-Auth-Email: ' . $this->Email, - 'X-Auth-Key: ' . $this->ApiKey, - ]; - } else { - $headers = [ - 'Authorization: Bearer ' . $this->ApiKey, - ]; - } - - $body = ''; - if ($method == 'GET' || $method == 'DELETE') { - 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'); - } - $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 ($arr) { - if ($arr['success']) { - return $arr; - } else { - $this->setError(isset($arr['errors'][0]) ? $arr['errors'][0]['message'] : '未知错误'); - return false; - } - } else { - $this->setError('返回数据解析失败'); - return false; - } - } - - private function setError($message) - { - $this->error = $message; - //file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND); - } -} +Email = $config['ak']; + $this->ApiKey = $config['sk']; + $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) + { + $param = ['page' => $PageNumber, 'per_page' => $PageSize]; + if (!empty($KeyWord)) { + $param['name'] = $KeyWord; + } + $data = $this->send_reuqest('GET', '/zones', $param); + if ($data) { + $list = []; + foreach ($data['result'] as $row) { + $list[] = [ + 'DomainId' => $row['id'], + 'Domain' => $row['name'], + 'RecordCount' => 0, + ]; + } + return ['total' => $data['result_info']['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 (!isNullOrEmpty($Value)) $KeyWord = $Value; + $param = ['type' => $Type, 'search' => $KeyWord, 'page' => $PageNumber, 'per_page' => $PageSize]; + if (!isNullOrEmpty($SubDomain)) { + if ($SubDomain == '@') $SubDomain = $this->domain; + else $SubDomain .= '.' . $this->domain; + $param['name'] = $SubDomain; + } + if (!isNullOrEmpty($Line)) { + $param['proxied'] = $Line == '1' ? 'true' : 'false'; + } + $data = $this->send_reuqest('GET', '/zones/'.$this->domainid.'/dns_records', $param); + if ($data) { + $list = []; + foreach ($data['result'] as $row) { + $name = $this->domain == $row['name'] ? '@' : str_replace('.'.$this->domain, '', $row['name']); + $status = str_ends_with($name, '_pause') ? '0' : '1'; + $name = $status == '0' ? substr($name, 0, -6) : $name; + $list[] = [ + 'RecordId' => $row['id'], + 'Domain' => $this->domain, + 'Name' => $name, + 'Type' => $row['type'], + 'Value' => $row['content'], + 'Line' => $row['proxied'] ? '1' : '0', + 'TTL' => $row['ttl'], + 'MX' => isset($row['priority']) ? $row['priority'] : null, + 'Status' => $status, + 'Weight' => null, + 'Remark' => $row['comment'], + 'UpdateTime' => $row['modified_on'], + ]; + } + return ['total' => $data['result_info']['total_count'], '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) + { + $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']); + $status = str_ends_with($name, '_pause') ? '0' : '1'; + $name = $status == '0' ? substr($name, 0, -6) : $name; + return [ + 'RecordId' => $data['result']['id'], + 'Domain' => $this->domain, + 'Name' => $name, + 'Type' => $data['result']['type'], + 'Value' => $data['result']['content'], + 'Line' => $data['result']['proxied'] ? '1' : '0', + 'TTL' => $data['result']['ttl'], + 'MX' => isset($data['result']['priority']) ? $data['result']['priority'] : null, + 'Status' => $status, + 'Weight' => null, + 'Remark' => $data['result']['comment'], + 'UpdateTime' => $data['result']['modified_on'], + ]; + } + return false; + } + + //添加解析记录 + public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $param = ['name' => $Name, 'type' => $this->convertType($Type), 'content' => $Value, 'proxied' => $Line == '1', 'ttl' => intval($TTL), 'comment' => $Remark]; + if ($Type == 'MX') $param['priority'] = intval($MX); + if ($Type == 'CAA' || $Type == 'SRV') { + unset($param['content']); + $param['data'] = $this->convertValue($Value, $Type); + } + $data = $this->send_reuqest('POST', '/zones/'.$this->domainid.'/dns_records', $param); + return is_array($data) ? $data['result']['id'] : false; + } + + //修改解析记录 + public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $param = ['name' => $Name, 'type' => $this->convertType($Type), 'content' => $Value, 'proxied' => $Line == '1', 'ttl' => intval($TTL), 'comment' => $Remark]; + if ($Type == 'MX') $param['priority'] = intval($MX); + if ($Type == 'CAA' || $Type == 'SRV') { + unset($param['content']); + $param['data'] = $this->convertValue($Value, $Type); + } + $data = $this->send_reuqest('PATCH', '/zones/'.$this->domainid.'/dns_records/'.$RecordId, $param); + return is_array($data); + } + + //修改解析记录备注 + public function updateDomainRecordRemark($RecordId, $Remark) + { + return false; + } + + //删除解析记录 + public function deleteDomainRecord($RecordId) + { + $data = $this->send_reuqest('DELETE', '/zones/'.$this->domainid.'/dns_records/'.$RecordId); + return is_array($data); + } + + //设置解析记录状态 + public function setDomainRecordStatus($RecordId, $Status) + { + $info = $this->getDomainRecordInfo($RecordId); + $Name = $Status == '1' ? str_replace('_pause', '', $info['Name']) : $info['Name'] . '_pause'; + return $this->updateDomainRecord($RecordId, $Name, $info['Type'], $info['Value'], $info['Line'], $info['TTL'], $info['MX'], $info['Weight'], $info['Remark']); + } + + //获取解析记录操作日志 + 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() + { + $data = $this->send_reuqest('GET', '/zones/'.$this->domainid); + if ($data) { + return $data['result']; + } + return false; + } + + //获取域名最低TTL + public function getMinTTL() + { + return false; + } + + public function addDomain($Domain) + { + $param = ['name' => $Domain]; + $data = $this->send_reuqest('POST', '/zones', $param); + if ($data) { + return ['id' => $data['result']['id'], 'name' => $data['result']['name']]; + } + return false; + } + + private function convertType($type) + { + $convert_dict = ['REDIRECT_URL' => 'URI', 'FORWARD_URL' => 'URI']; + if (array_key_exists($type, $convert_dict)) { + return $convert_dict[$type]; + } + return $type; + } + + private function convertValue($value, $type) + { + if ($type == 'SRV') { + $arr = explode(' ', $value); + if (count($arr) > 3) { + $data = [ + 'priority' => intval($arr[0]), + 'weight' => intval($arr[1]), + 'port' => intval($arr[2]), + 'target' => $arr[3], + ]; + } else { + $data = [ + 'weight' => intval($arr[0]), + 'port' => intval($arr[1]), + 'target' => $arr[2], + ]; + } + } elseif ($type == 'CAA') { + $arr = explode(' ', $value); + $data = [ + 'flags' => intval($arr[0]), + 'tag' => $arr[1], + 'value' => trim($arr[2], '"'), + ]; + } + return $data; + } + + private function send_reuqest($method, $path, $params = null) + { + $url = $this->baseUrl . $path; + + if (preg_match('/^[0-9a-z]+$/i', $this->ApiKey)) { + $headers = [ + 'X-Auth-Email: ' . $this->Email, + 'X-Auth-Key: ' . $this->ApiKey, + ]; + } else { + $headers = [ + 'Authorization: Bearer ' . $this->ApiKey, + ]; + } + + $body = ''; + if ($method == 'GET' || $method == 'DELETE') { + 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'); + } + $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 ($arr) { + if ($arr['success']) { + return $arr; + } else { + $this->setError(isset($arr['errors'][0]) ? $arr['errors'][0]['message'] : '未知错误'); + return false; + } + } else { + $this->setError('返回数据解析失败'); + return false; + } + } + + private function setError($message) + { + $this->error = $message; + //file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND); + } +} diff --git a/app/lib/dns/dnsla.php b/app/lib/dns/dnsla.php index 75c8fe1..cc57513 100644 --- a/app/lib/dns/dnsla.php +++ b/app/lib/dns/dnsla.php @@ -1,304 +1,304 @@ - 'A', 2 => 'NS', 5 => 'CNAME', 15 => 'MX', 16 => 'TXT', 28 => 'AAAA', 33 => 'SRV', 257 => 'CAA', 256 => 'URL转发']; - private $error; - private $domain; - private $domainid; - private $proxy; - - public function __construct($config) - { - $this->apiid = $config['ak']; - $this->apisecret = $config['sk']; - $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) - { - $param = ['pageIndex' => $PageNumber, 'pageSize' => $PageSize]; - $data = $this->execute('GET', '/api/domainList', $param); - if ($data) { - $list = []; - foreach ($data['results'] as $row) { - $list[] = [ - 'DomainId' => $row['id'], - 'Domain' => rtrim($row['displayDomain'], '.'), - 'RecordCount' => 0, - ]; - } - return ['total' => $data['total'], 'list' => $list]; - } - return false; - } - - //获取解析记录列表 - public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null) - { - $param = ['domainId' => $this->domainid, 'pageIndex' => $PageNumber, 'pageSize' => $PageSize]; - if (!isNullOrEmpty(($KeyWord))) { - $param['host'] = $KeyWord; - } - if (!isNullOrEmpty(($Type))) { - $param['type'] = $this->convertType($Type); - } - if (!isNullOrEmpty(($Line))) { - $param['lineId'] = $Line; - } - if (!isNullOrEmpty(($SubDomain))) { - $param['host'] = $SubDomain; - } - if (!isNullOrEmpty(($Value))) { - $param['data'] = $Value; - } - $data = $this->execute('GET', '/api/recordList', $param); - if ($data) { - $list = []; - foreach ($data['results'] as $row) { - $list[] = [ - 'RecordId' => $row['id'], - 'Domain' => $this->domain, - 'Name' => $row['host'], - 'Type' => $this->convertTypeId($row['type'], isset($row['domaint']) ? $row['domaint'] : false), - 'Value' => $row['data'], - 'Line' => $row['lineId'], - 'TTL' => $row['ttl'], - 'MX' => isset($row['preference']) ? $row['preference'] : null, - 'Status' => $row['disable'] ? '0' : '1', - 'Weight' => isset($row['weight']) ? $row['weight'] : null, - 'Remark' => null, - 'UpdateTime' => date('Y-m-d H:i:s', $row['updatedAt']), - ]; - } - return ['total' => $data['total'], 'list' => $list]; - } - return false; - } - - //获取子域名解析记录列表 - public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null) - { - if ($SubDomain == '') $SubDomain = '@'; - return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line); - } - - //获取解析记录详细信息 - public function getDomainRecordInfo($RecordId) - { - return false; - } - - //添加解析记录 - public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $param = ['domainId' => $this->domainid, 'type' => $this->convertType($Type), 'host' => $Name, 'data' => $Value, 'ttl' => intval($TTL), 'lineId' => $Line]; - if ($Type == 'MX') $param['preference'] = intval($MX); - if ($Type == 'REDIRECT_URL') { - $param['type'] = 256; - $param['dominant'] = true; - } elseif ($Type == 'FORWARD_URL') { - $param['type'] = 256; - $param['dominant'] = false; - } - if ($Weight > 0) $param['weight'] = $Weight; - $data = $this->execute('POST', '/api/record', $param); - return is_array($data) ? $data['id'] : false; - } - - //修改解析记录 - public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $param = ['id' => $RecordId, 'type' => $this->convertType($Type), 'host' => $Name, 'data' => $Value, 'ttl' => intval($TTL), 'lineId' => $Line]; - if ($Type == 'MX') $param['preference'] = intval($MX); - if ($Type == 'REDIRECT_URL') { - $param['type'] = 256; - $param['dominant'] = true; - } elseif ($Type == 'FORWARD_URL') { - $param['type'] = 256; - $param['dominant'] = false; - } - if ($Weight > 0) $param['weight'] = $Weight; - $data = $this->execute('PUT', '/api/record', $param); - return $data !== false; - } - - //修改解析记录备注 - public function updateDomainRecordRemark($RecordId, $Remark) - { - return false; - } - - //删除解析记录 - public function deleteDomainRecord($RecordId) - { - $param = ['id' => $RecordId]; - $data = $this->execute('DELETE', '/api/record', $param); - return $data !== false; - } - - //设置解析记录状态 - public function setDomainRecordStatus($RecordId, $Status) - { - $param = ['id' => $RecordId, 'disable' => $Status == '0' ? true : false]; - $data = $this->execute('PUT', '/api/recordDisable', $param); - return $data !== false; - } - - //获取解析记录操作日志 - public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) - { - return false; - } - - //获取解析线路列表 - public function getRecordLine() - { - $param = ['domain' => $this->domain]; - $data = $this->execute('GET', '/api/availableLine', $param); - if ($data) { - array_multisort(array_column($data, 'order'), SORT_ASC, $data); - $list = []; - foreach ($data as $row) { - if ($row['id'] == '0') $row['id'] = ''; - $list[$row['id']] = ['name' => $row['value'], 'parent' => !empty($row['pid']) ? $row['pid'] : null]; - } - return $list; - } - return false; - } - - //获取域名信息 - public function getDomainInfo() - { - $param = ['id' => $this->domainid]; - $data = $this->execute('GET', '/api/domain', $param); - return $data; - } - - //获取域名最低TTL - public function getMinTTL() - { - $param = ['id' => $this->domainid]; - $data = $this->execute('GET', '/api/dnsMeasures', $param); - if ($data && isset($data['minTTL'])) { - return $data['minTTL']; - } - return false; - } - - public function addDomain($Domain) - { - $param = ['domain' => $Domain]; - $data = $this->execute('POST', '/api/domain', $param); - if ($data) { - return ['id' => $data['id'], 'name' => $Domain]; - } - return false; - } - - private function convertType($type) - { - $typeList = array_flip($this->typeList); - return $typeList[$type]; - } - - private function convertTypeId($typeId, $domaint) - { - if ($typeId == 256) return $domaint ? 'REDIRECT_URL' : 'FORWARD_URL'; - return $this->typeList[$typeId]; - } - - 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']; - if ($method == 'POST' || $method == 'PUT') { - $response = $this->curl($method, $path, $header, json_encode($params)); - } else { - if ($params) { - $path .= '?'.http_build_query($params); - } - $response = $this->curl($method, $path, $header); - } - if (!$response) { - return false; - } - $arr = json_decode($response, true); - if ($arr) { - if ($arr['code'] == 200) { - 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; - $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 ($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) { - $this->setError('认证失败'); - return false; - } else { - $this->setError('http code: '.$httpCode); - return false; - } - } - - private function setError($message) - { - $this->error = $message; - //file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND); - } -} + 'A', 2 => 'NS', 5 => 'CNAME', 15 => 'MX', 16 => 'TXT', 28 => 'AAAA', 33 => 'SRV', 257 => 'CAA', 256 => 'URL转发']; + private $error; + private $domain; + private $domainid; + private $proxy; + + public function __construct($config) + { + $this->apiid = $config['ak']; + $this->apisecret = $config['sk']; + $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) + { + $param = ['pageIndex' => $PageNumber, 'pageSize' => $PageSize]; + $data = $this->execute('GET', '/api/domainList', $param); + if ($data) { + $list = []; + foreach ($data['results'] as $row) { + $list[] = [ + 'DomainId' => $row['id'], + 'Domain' => rtrim($row['displayDomain'], '.'), + 'RecordCount' => 0, + ]; + } + return ['total' => $data['total'], 'list' => $list]; + } + return false; + } + + //获取解析记录列表 + public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null) + { + $param = ['domainId' => $this->domainid, 'pageIndex' => $PageNumber, 'pageSize' => $PageSize]; + if (!isNullOrEmpty(($KeyWord))) { + $param['host'] = $KeyWord; + } + if (!isNullOrEmpty(($Type))) { + $param['type'] = $this->convertType($Type); + } + if (!isNullOrEmpty(($Line))) { + $param['lineId'] = $Line; + } + if (!isNullOrEmpty(($SubDomain))) { + $param['host'] = $SubDomain; + } + if (!isNullOrEmpty(($Value))) { + $param['data'] = $Value; + } + $data = $this->execute('GET', '/api/recordList', $param); + if ($data) { + $list = []; + foreach ($data['results'] as $row) { + $list[] = [ + 'RecordId' => $row['id'], + 'Domain' => $this->domain, + 'Name' => $row['host'], + 'Type' => $this->convertTypeId($row['type'], isset($row['domaint']) ? $row['domaint'] : false), + 'Value' => $row['data'], + 'Line' => $row['lineId'], + 'TTL' => $row['ttl'], + 'MX' => isset($row['preference']) ? $row['preference'] : null, + 'Status' => $row['disable'] ? '0' : '1', + 'Weight' => isset($row['weight']) ? $row['weight'] : null, + 'Remark' => null, + 'UpdateTime' => date('Y-m-d H:i:s', $row['updatedAt']), + ]; + } + return ['total' => $data['total'], 'list' => $list]; + } + return false; + } + + //获取子域名解析记录列表 + public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null) + { + if ($SubDomain == '') $SubDomain = '@'; + return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line); + } + + //获取解析记录详细信息 + public function getDomainRecordInfo($RecordId) + { + return false; + } + + //添加解析记录 + public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $param = ['domainId' => $this->domainid, 'type' => $this->convertType($Type), 'host' => $Name, 'data' => $Value, 'ttl' => intval($TTL), 'lineId' => $Line]; + if ($Type == 'MX') $param['preference'] = intval($MX); + if ($Type == 'REDIRECT_URL') { + $param['type'] = 256; + $param['dominant'] = true; + } elseif ($Type == 'FORWARD_URL') { + $param['type'] = 256; + $param['dominant'] = false; + } + if ($Weight > 0) $param['weight'] = $Weight; + $data = $this->execute('POST', '/api/record', $param); + return is_array($data) ? $data['id'] : false; + } + + //修改解析记录 + public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $param = ['id' => $RecordId, 'type' => $this->convertType($Type), 'host' => $Name, 'data' => $Value, 'ttl' => intval($TTL), 'lineId' => $Line]; + if ($Type == 'MX') $param['preference'] = intval($MX); + if ($Type == 'REDIRECT_URL') { + $param['type'] = 256; + $param['dominant'] = true; + } elseif ($Type == 'FORWARD_URL') { + $param['type'] = 256; + $param['dominant'] = false; + } + if ($Weight > 0) $param['weight'] = $Weight; + $data = $this->execute('PUT', '/api/record', $param); + return $data !== false; + } + + //修改解析记录备注 + public function updateDomainRecordRemark($RecordId, $Remark) + { + return false; + } + + //删除解析记录 + public function deleteDomainRecord($RecordId) + { + $param = ['id' => $RecordId]; + $data = $this->execute('DELETE', '/api/record', $param); + return $data !== false; + } + + //设置解析记录状态 + public function setDomainRecordStatus($RecordId, $Status) + { + $param = ['id' => $RecordId, 'disable' => $Status == '0' ? true : false]; + $data = $this->execute('PUT', '/api/recordDisable', $param); + return $data !== false; + } + + //获取解析记录操作日志 + public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) + { + return false; + } + + //获取解析线路列表 + public function getRecordLine() + { + $param = ['domain' => $this->domain]; + $data = $this->execute('GET', '/api/availableLine', $param); + if ($data) { + array_multisort(array_column($data, 'order'), SORT_ASC, $data); + $list = []; + foreach ($data as $row) { + if ($row['id'] == '0') $row['id'] = ''; + $list[$row['id']] = ['name' => $row['value'], 'parent' => !empty($row['pid']) ? $row['pid'] : null]; + } + return $list; + } + return false; + } + + //获取域名信息 + public function getDomainInfo() + { + $param = ['id' => $this->domainid]; + $data = $this->execute('GET', '/api/domain', $param); + return $data; + } + + //获取域名最低TTL + public function getMinTTL() + { + $param = ['id' => $this->domainid]; + $data = $this->execute('GET', '/api/dnsMeasures', $param); + if ($data && isset($data['minTTL'])) { + return $data['minTTL']; + } + return false; + } + + public function addDomain($Domain) + { + $param = ['domain' => $Domain]; + $data = $this->execute('POST', '/api/domain', $param); + if ($data) { + return ['id' => $data['id'], 'name' => $Domain]; + } + return false; + } + + private function convertType($type) + { + $typeList = array_flip($this->typeList); + return $typeList[$type]; + } + + private function convertTypeId($typeId, $domaint) + { + if ($typeId == 256) return $domaint ? 'REDIRECT_URL' : 'FORWARD_URL'; + return $this->typeList[$typeId]; + } + + 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']; + if ($method == 'POST' || $method == 'PUT') { + $response = $this->curl($method, $path, $header, json_encode($params)); + } else { + if ($params) { + $path .= '?'.http_build_query($params); + } + $response = $this->curl($method, $path, $header); + } + if (!$response) { + return false; + } + $arr = json_decode($response, true); + if ($arr) { + if ($arr['code'] == 200) { + 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; + $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 ($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) { + $this->setError('认证失败'); + return false; + } else { + $this->setError('http code: '.$httpCode); + return false; + } + } + + private function setError($message) + { + $this->error = $message; + //file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND); + } +} diff --git a/app/lib/dns/dnspod.php b/app/lib/dns/dnspod.php index 8e246aa..909155f 100644 --- a/app/lib/dns/dnspod.php +++ b/app/lib/dns/dnspod.php @@ -1,373 +1,373 @@ -SecretId = $config['ak']; - $this->SecretKey = $config['sk']; - $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']; - } - - 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 = 'DescribeDomainList'; - $offset = ($PageNumber - 1) * $PageSize; - $param = ['Offset' => $offset, 'Limit' => $PageSize, 'Keyword' => $KeyWord]; - $data = $this->send_request($action, $param); - if ($data) { - $list = []; - foreach ($data['DomainList'] as $row) { - $list[] = [ - 'DomainId' => $row['DomainId'], - 'Domain' => $row['Name'], - 'RecordCount' => $row['RecordCount'], - ]; - } - return ['total' => $data['DomainCountInfo']['DomainTotal'], '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; - if (!isNullOrEmpty($Status) || !isNullOrEmpty($Value)) { - $action = 'DescribeRecordFilterList'; - $param = ['Domain' => $this->domain, 'Offset' => $offset, 'Limit' => $PageSize, 'RecordValue' => $Value]; - if (!isNullOrEmpty($SubDomain)) $param['SubDomain'] = $SubDomain; - if (!isNullOrEmpty($KeyWord)) $param['Keyword'] = $KeyWord; - if (!isNullOrEmpty($Value)) $param['RecordValue'] = $Value; - if (!isNullOrEmpty($Status)) { - $Status = $Status == '1' ? 'ENABLE' : 'DISABLE'; - $param['RecordStatus'] = [$Status]; - } - if (!isNullOrEmpty($Type)) $param['RecordType'] = [$this->convertType($Type)]; - if (!isNullOrEmpty($Line)) $param['RecordLine'] = [$Line]; - } else { - $action = 'DescribeRecordList'; - $param = ['Domain' => $this->domain, 'Subdomain' => $SubDomain, 'RecordType' => $this->convertType($Type), 'RecordLineId' => $Line, 'Keyword' => $KeyWord, 'Offset' => $offset, 'Limit' => $PageSize]; - } - $data = $this->send_request($action, $param); - if ($data) { - $list = []; - foreach ($data['RecordList'] as $row) { - //if($row['Name'] == '@' && $row['Type'] == 'NS') continue; - $list[] = [ - 'RecordId' => $row['RecordId'], - 'Domain' => $this->domain, - 'Name' => $row['Name'], - 'Type' => $this->convertTypeId($row['Type']), - 'Value' => $row['Value'], - 'Line' => $row['LineId'], - 'TTL' => $row['TTL'], - 'MX' => $row['MX'], - 'Status' => $row['Status'] == 'ENABLE' ? '1' : '0', - 'Weight' => $row['Weight'], - 'Remark' => $row['Remark'], - 'UpdateTime' => $row['UpdatedOn'], - ]; - } - return ['total' => $data['RecordCountInfo']['TotalCount'], 'list' => $list]; - } elseif ($this->error == '记录列表为空。' || $this->error == 'No records on the list.') { - return ['total' => 0, '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 = 'DescribeRecord'; - $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId)]; - $data = $this->send_request($action, $param); - if ($data) { - return [ - 'RecordId' => $data['RecordInfo']['Id'], - 'Domain' => $this->domain, - 'Name' => $data['RecordInfo']['SubDomain'], - 'Type' => $this->convertTypeId($data['RecordInfo']['RecordType']), - 'Value' => $data['RecordInfo']['Value'], - 'Line' => $data['RecordInfo']['RecordLineId'], - 'TTL' => $data['RecordInfo']['TTL'], - 'MX' => $data['RecordInfo']['MX'], - 'Status' => $data['RecordInfo']['Enabled'] == 1 ? '1' : '0', - 'Weight' => $data['RecordInfo']['Weight'], - 'Remark' => $data['RecordInfo']['Remark'], - 'UpdateTime' => $data['RecordInfo']['UpdatedOn'], - ]; - } - return false; - } - - //添加解析记录 - public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $action = 'CreateRecord'; - $param = ['Domain' => $this->domain, 'SubDomain' => $Name, 'RecordType' => $this->convertType($Type), 'Value' => $Value, 'RecordLine' => $Line, 'RecordLineId' => $this->convertLineCode($Line), 'TTL' => intval($TTL), 'Weight' => $Weight]; - if ($Type == 'MX') $param['MX'] = 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 = 'ModifyRecord'; - $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId), 'SubDomain' => $Name, 'RecordType' => $this->convertType($Type), 'Value' => $Value, 'RecordLine' => $Line, 'RecordLineId' => $this->convertLineCode($Line), 'TTL' => intval($TTL), 'Weight' => $Weight]; - if ($Type == 'MX') $param['MX'] = intval($MX); - $data = $this->send_request($action, $param); - return is_array($data); - } - - //修改解析记录备注 - public function updateDomainRecordRemark($RecordId, $Remark) - { - $action = 'ModifyRecordRemark'; - $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId), 'Remark' => $Remark]; - $data = $this->send_request($action, $param); - return is_array($data); - } - - //删除解析记录 - public function deleteDomainRecord($RecordId) - { - $action = 'DeleteRecord'; - $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId)]; - $data = $this->send_request($action, $param); - return is_array($data); - } - - //设置解析记录状态 - public function setDomainRecordStatus($RecordId, $Status) - { - $Status = $Status == '1' ? 'ENABLE' : 'DISABLE'; - $action = 'ModifyRecordStatus'; - $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId), 'Status' => $Status]; - $data = $this->send_request($action, $param); - return is_array($data); - } - - //获取解析记录操作日志 - public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) - { - $action = 'DescribeDomainLogList'; - $offset = ($PageNumber - 1) * $PageSize; - $param = ['Domain' => $this->domain, 'Offset' => $offset, 'Limit' => $PageSize]; - $data = $this->send_request($action, $param); - if ($data) { - $list = []; - foreach ($data['LogList'] as $row) { - $list[] = ['time' => substr($row, 0, strpos($row, '(')), 'ip' => substr($row, strpos($row, '(') + 1, strpos($row, ')') - strpos($row, '(') - 1), 'data' => substr($row, strpos($row, ')') + 1, strpos($row, ' Uin:') - strpos($row, ')') - 1)]; - } - return ['total' => $data['TotalCount'], 'list' => $list]; - } - return false; - } - - //获取解析线路列表 - public function getRecordLine() - { - $action = 'DescribeRecordLineCategoryList'; - $param = ['Domain' => $this->domain]; - $data = $this->send_request($action, $param); - if ($data) { - $list = []; - $this->processLineList($list, $data['LineList'], null); - return $list; - } else { - $data = $this->getRecordLineByGrade(); - if ($data) { - $list = []; - foreach ($data as $row) { - $list[$row['LineId']] = ['name' => $row['Name'], 'parent' => null]; - } - return $list; - } - } - return false; - } - - private function processLineList(&$list, $line_list, $parent) - { - foreach ($line_list as $row) { - if (isNullOrEmpty($row['LineId'])) $row['LineId'] = 'N.' . $row['LineName']; - if ($row['Useful'] && !isset($list[$row['LineId']])) { - $list[$row['LineId']] = ['name' => $row['LineName'], 'parent' => $parent]; - if ($row['SubGroup']) { - $this->processLineList($list, $row['SubGroup'], $row['LineId']); - } - } - } - } - - //获取域名概览信息 - public function getDomainInfo() - { - $action = 'DescribeDomain'; - $param = ['Domain' => $this->domain]; - $data = $this->send_request($action, $param); - if ($data) { - $this->domainInfo = $data['DomainInfo']; - return $data['DomainInfo']; - } - return false; - } - - //获取域名权限 - public function getDomainPurview() - { - $action = 'DescribeDomainPurview'; - $param = ['Domain' => $this->domain]; - $data = $this->send_request($action, $param); - if ($data) { - return $data['PurviewList']; - } - return false; - } - - //获取域名最低TTL - public function getMinTTL() - { - if ($this->domainInfo) { - return $this->domainInfo['TTL']; - } - $PurviewList = $this->getDomainPurview(); - if ($PurviewList) { - foreach ($PurviewList as $row) { - if ($row['Name'] == '记录 TTL 最低' || $row['Name'] == 'Min TTL value') { - return intval($row['Value']); - } - } - } - return false; - } - - //获取等级允许的线路 - public function getRecordLineByGrade() - { - $action = 'DescribeRecordLineList'; - $param = ['Domain' => $this->domain, 'DomainGrade' => '']; - $data = $this->send_request($action, $param); - if ($data) { - $line_list = $data['LineList']; - if (!empty($data['LineGroupList'])) { - foreach ($data['LineGroupList'] as $row) { - $line_list[] = ['Name' => $row['Name'], 'LineId' => $row['LineId']]; - } - } - return $line_list; - } - return false; - } - - //获取用户信息 - public function getAccountInfo() - { - $action = 'DescribeUserDetail'; - $param = []; - $data = $this->send_request($action, $param); - if ($data) { - return $data['UserInfo']; - } - return false; - } - - public function addDomain($Domain) - { - $action = 'CreateDomain'; - $param = [ - 'Domain' => $Domain, - ]; - $data = $this->send_request($action, $param); - if ($data) { - return ['id' => $data['DomainInfo']['Id'], 'name' => $data['DomainInfo']['Domain']]; - } - return false; - } - - private function convertLineCode($line) - { - $convert_dict = ['default' => '0', 'unicom' => '10=1', 'telecom' => '10=0', 'mobile' => '10=3', 'edu' => '10=2', 'oversea' => '3=0', 'btvn' => '10=22', 'search' => '80=0', 'internal' => '7=0']; - if (array_key_exists($line, $convert_dict)) { - return $convert_dict[$line]; - } - return $line; - } - - private function convertType($type) - { - $convert_dict = ['REDIRECT_URL' => '显性URL', 'FORWARD_URL' => '隐性URL']; - if (array_key_exists($type, $convert_dict)) { - return $convert_dict[$type]; - } - return $type; - } - - private function convertTypeId($type) - { - $convert_dict = ['显性URL' => 'REDIRECT_URL', '隐性URL' => 'FORWARD_URL']; - if (array_key_exists($type, $convert_dict)) { - return $convert_dict[$type]; - } - return $type; - } - - - 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); - } -} +SecretId = $config['ak']; + $this->SecretKey = $config['sk']; + $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']; + } + + 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 = 'DescribeDomainList'; + $offset = ($PageNumber - 1) * $PageSize; + $param = ['Offset' => $offset, 'Limit' => $PageSize, 'Keyword' => $KeyWord]; + $data = $this->send_request($action, $param); + if ($data) { + $list = []; + foreach ($data['DomainList'] as $row) { + $list[] = [ + 'DomainId' => $row['DomainId'], + 'Domain' => $row['Name'], + 'RecordCount' => $row['RecordCount'], + ]; + } + return ['total' => $data['DomainCountInfo']['DomainTotal'], '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; + if (!isNullOrEmpty($Status) || !isNullOrEmpty($Value)) { + $action = 'DescribeRecordFilterList'; + $param = ['Domain' => $this->domain, 'Offset' => $offset, 'Limit' => $PageSize, 'RecordValue' => $Value]; + if (!isNullOrEmpty($SubDomain)) $param['SubDomain'] = $SubDomain; + if (!isNullOrEmpty($KeyWord)) $param['Keyword'] = $KeyWord; + if (!isNullOrEmpty($Value)) $param['RecordValue'] = $Value; + if (!isNullOrEmpty($Status)) { + $Status = $Status == '1' ? 'ENABLE' : 'DISABLE'; + $param['RecordStatus'] = [$Status]; + } + if (!isNullOrEmpty($Type)) $param['RecordType'] = [$this->convertType($Type)]; + if (!isNullOrEmpty($Line)) $param['RecordLine'] = [$Line]; + } else { + $action = 'DescribeRecordList'; + $param = ['Domain' => $this->domain, 'Subdomain' => $SubDomain, 'RecordType' => $this->convertType($Type), 'RecordLineId' => $Line, 'Keyword' => $KeyWord, 'Offset' => $offset, 'Limit' => $PageSize]; + } + $data = $this->send_request($action, $param); + if ($data) { + $list = []; + foreach ($data['RecordList'] as $row) { + //if($row['Name'] == '@' && $row['Type'] == 'NS') continue; + $list[] = [ + 'RecordId' => $row['RecordId'], + 'Domain' => $this->domain, + 'Name' => $row['Name'], + 'Type' => $this->convertTypeId($row['Type']), + 'Value' => $row['Value'], + 'Line' => $row['LineId'], + 'TTL' => $row['TTL'], + 'MX' => $row['MX'], + 'Status' => $row['Status'] == 'ENABLE' ? '1' : '0', + 'Weight' => $row['Weight'], + 'Remark' => $row['Remark'], + 'UpdateTime' => $row['UpdatedOn'], + ]; + } + return ['total' => $data['RecordCountInfo']['TotalCount'], 'list' => $list]; + } elseif ($this->error == '记录列表为空。' || $this->error == 'No records on the list.') { + return ['total' => 0, '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 = 'DescribeRecord'; + $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId)]; + $data = $this->send_request($action, $param); + if ($data) { + return [ + 'RecordId' => $data['RecordInfo']['Id'], + 'Domain' => $this->domain, + 'Name' => $data['RecordInfo']['SubDomain'], + 'Type' => $this->convertTypeId($data['RecordInfo']['RecordType']), + 'Value' => $data['RecordInfo']['Value'], + 'Line' => $data['RecordInfo']['RecordLineId'], + 'TTL' => $data['RecordInfo']['TTL'], + 'MX' => $data['RecordInfo']['MX'], + 'Status' => $data['RecordInfo']['Enabled'] == 1 ? '1' : '0', + 'Weight' => $data['RecordInfo']['Weight'], + 'Remark' => $data['RecordInfo']['Remark'], + 'UpdateTime' => $data['RecordInfo']['UpdatedOn'], + ]; + } + return false; + } + + //添加解析记录 + public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $action = 'CreateRecord'; + $param = ['Domain' => $this->domain, 'SubDomain' => $Name, 'RecordType' => $this->convertType($Type), 'Value' => $Value, 'RecordLine' => $Line, 'RecordLineId' => $this->convertLineCode($Line), 'TTL' => intval($TTL), 'Weight' => $Weight]; + if ($Type == 'MX') $param['MX'] = 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 = 'ModifyRecord'; + $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId), 'SubDomain' => $Name, 'RecordType' => $this->convertType($Type), 'Value' => $Value, 'RecordLine' => $Line, 'RecordLineId' => $this->convertLineCode($Line), 'TTL' => intval($TTL), 'Weight' => $Weight]; + if ($Type == 'MX') $param['MX'] = intval($MX); + $data = $this->send_request($action, $param); + return is_array($data); + } + + //修改解析记录备注 + public function updateDomainRecordRemark($RecordId, $Remark) + { + $action = 'ModifyRecordRemark'; + $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId), 'Remark' => $Remark]; + $data = $this->send_request($action, $param); + return is_array($data); + } + + //删除解析记录 + public function deleteDomainRecord($RecordId) + { + $action = 'DeleteRecord'; + $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId)]; + $data = $this->send_request($action, $param); + return is_array($data); + } + + //设置解析记录状态 + public function setDomainRecordStatus($RecordId, $Status) + { + $Status = $Status == '1' ? 'ENABLE' : 'DISABLE'; + $action = 'ModifyRecordStatus'; + $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId), 'Status' => $Status]; + $data = $this->send_request($action, $param); + return is_array($data); + } + + //获取解析记录操作日志 + public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) + { + $action = 'DescribeDomainLogList'; + $offset = ($PageNumber - 1) * $PageSize; + $param = ['Domain' => $this->domain, 'Offset' => $offset, 'Limit' => $PageSize]; + $data = $this->send_request($action, $param); + if ($data) { + $list = []; + foreach ($data['LogList'] as $row) { + $list[] = ['time' => substr($row, 0, strpos($row, '(')), 'ip' => substr($row, strpos($row, '(') + 1, strpos($row, ')') - strpos($row, '(') - 1), 'data' => substr($row, strpos($row, ')') + 1, strpos($row, ' Uin:') - strpos($row, ')') - 1)]; + } + return ['total' => $data['TotalCount'], 'list' => $list]; + } + return false; + } + + //获取解析线路列表 + public function getRecordLine() + { + $action = 'DescribeRecordLineCategoryList'; + $param = ['Domain' => $this->domain]; + $data = $this->send_request($action, $param); + if ($data) { + $list = []; + $this->processLineList($list, $data['LineList'], null); + return $list; + } else { + $data = $this->getRecordLineByGrade(); + if ($data) { + $list = []; + foreach ($data as $row) { + $list[$row['LineId']] = ['name' => $row['Name'], 'parent' => null]; + } + return $list; + } + } + return false; + } + + private function processLineList(&$list, $line_list, $parent) + { + foreach ($line_list as $row) { + if (isNullOrEmpty($row['LineId'])) $row['LineId'] = 'N.' . $row['LineName']; + if ($row['Useful'] && !isset($list[$row['LineId']])) { + $list[$row['LineId']] = ['name' => $row['LineName'], 'parent' => $parent]; + if ($row['SubGroup']) { + $this->processLineList($list, $row['SubGroup'], $row['LineId']); + } + } + } + } + + //获取域名概览信息 + public function getDomainInfo() + { + $action = 'DescribeDomain'; + $param = ['Domain' => $this->domain]; + $data = $this->send_request($action, $param); + if ($data) { + $this->domainInfo = $data['DomainInfo']; + return $data['DomainInfo']; + } + return false; + } + + //获取域名权限 + public function getDomainPurview() + { + $action = 'DescribeDomainPurview'; + $param = ['Domain' => $this->domain]; + $data = $this->send_request($action, $param); + if ($data) { + return $data['PurviewList']; + } + return false; + } + + //获取域名最低TTL + public function getMinTTL() + { + if ($this->domainInfo) { + return $this->domainInfo['TTL']; + } + $PurviewList = $this->getDomainPurview(); + if ($PurviewList) { + foreach ($PurviewList as $row) { + if ($row['Name'] == '记录 TTL 最低' || $row['Name'] == 'Min TTL value') { + return intval($row['Value']); + } + } + } + return false; + } + + //获取等级允许的线路 + public function getRecordLineByGrade() + { + $action = 'DescribeRecordLineList'; + $param = ['Domain' => $this->domain, 'DomainGrade' => '']; + $data = $this->send_request($action, $param); + if ($data) { + $line_list = $data['LineList']; + if (!empty($data['LineGroupList'])) { + foreach ($data['LineGroupList'] as $row) { + $line_list[] = ['Name' => $row['Name'], 'LineId' => $row['LineId']]; + } + } + return $line_list; + } + return false; + } + + //获取用户信息 + public function getAccountInfo() + { + $action = 'DescribeUserDetail'; + $param = []; + $data = $this->send_request($action, $param); + if ($data) { + return $data['UserInfo']; + } + return false; + } + + public function addDomain($Domain) + { + $action = 'CreateDomain'; + $param = [ + 'Domain' => $Domain, + ]; + $data = $this->send_request($action, $param); + if ($data) { + return ['id' => $data['DomainInfo']['Id'], 'name' => $data['DomainInfo']['Domain']]; + } + return false; + } + + private function convertLineCode($line) + { + $convert_dict = ['default' => '0', 'unicom' => '10=1', 'telecom' => '10=0', 'mobile' => '10=3', 'edu' => '10=2', 'oversea' => '3=0', 'btvn' => '10=22', 'search' => '80=0', 'internal' => '7=0']; + if (array_key_exists($line, $convert_dict)) { + return $convert_dict[$line]; + } + return $line; + } + + private function convertType($type) + { + $convert_dict = ['REDIRECT_URL' => '显性URL', 'FORWARD_URL' => '隐性URL']; + if (array_key_exists($type, $convert_dict)) { + return $convert_dict[$type]; + } + return $type; + } + + private function convertTypeId($type) + { + $convert_dict = ['显性URL' => 'REDIRECT_URL', '隐性URL' => 'FORWARD_URL']; + if (array_key_exists($type, $convert_dict)) { + return $convert_dict[$type]; + } + return $type; + } + + + 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); + } +} diff --git a/app/lib/dns/huawei.php b/app/lib/dns/huawei.php index b81d1eb..11a31ba 100644 --- a/app/lib/dns/huawei.php +++ b/app/lib/dns/huawei.php @@ -1,272 +1,272 @@ -AccessKeyId = $config['ak']; - $this->SecretAccessKey = $config['sk']; - $proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - $this->client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $proxy); - $this->domain = $config['domain']; - $this->domainid = $config['domainid']; - } - - public function getError() - { - return $this->error; - } - - public function check() - { - if ($this->getDomainList() != false) { - return true; - } - return false; - } - - //获取域名列表 - public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20) - { - $offset = ($PageNumber - 1) * $PageSize; - $query = ['offset' => $offset, 'limit' => $PageSize, 'name' => $KeyWord]; - $data = $this->send_request('GET', '/v2/zones', $query); - if ($data) { - $list = []; - foreach ($data['zones'] as $row) { - $list[] = [ - 'DomainId' => $row['id'], - 'Domain' => rtrim($row['name'], '.'), - 'RecordCount' => $row['record_num'], - ]; - } - return ['total' => $data['metadata']['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) - { - $offset = ($PageNumber - 1) * $PageSize; - $query = ['type' => $Type, 'line_id' => $Line, 'name' => $KeyWord, 'offset' => $offset, 'limit' => $PageSize]; - if (!isNullOrEmpty($Status)) { - $Status = $Status == '1' ? 'ACTIVE' : 'DISABLE'; - $query['status'] = $Status; - } - if (!isNullOrEmpty($SubDomain)) { - $SubDomain = $this->getHost($SubDomain); - $query['name'] = $SubDomain; - $query['search_mode'] = 'equal'; - } - $data = $this->send_request('GET', '/v2.1/zones/'.$this->domainid.'/recordsets', $query); - if ($data) { - $list = []; - foreach ($data['recordsets'] as $row) { - if ($row['name'] == $row['zone_name']) $row['name'] = '@'; - if ($row['type'] == 'MX') list($row['mx'], $row['records']) = explode(' ', $row['records'][0]); - $list[] = [ - 'RecordId' => $row['id'], - 'Domain' => rtrim($row['zone_name'], '.'), - 'Name' => str_replace('.'.$row['zone_name'], '', $row['name']), - 'Type' => $row['type'], - 'Value' => $row['records'], - 'Line' => $row['line'], - 'TTL' => $row['ttl'], - 'MX' => isset($row['mx']) ? $row['mx'] : null, - 'Status' => $row['status'] == 'ACTIVE' ? '1' : '0', - 'Weight' => $row['weight'], - 'Remark' => $row['description'], - 'UpdateTime' => $row['updated_at'], - ]; - } - return ['total' => $data['metadata']['total_count'], '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) - { - $data = $this->send_request('GET', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId); - if ($data) { - if ($data['name'] == $data['zone_name']) $data['name'] = '@'; - if ($data['type'] == 'MX') list($data['mx'], $data['records']) = explode(' ', $data['records'][0]); - return [ - 'RecordId' => $data['id'], - 'Domain' => rtrim($data['zone_name'], '.'), - 'Name' => str_replace('.'.$data['zone_name'], '', $data['name']), - 'Type' => $data['type'], - 'Value' => $data['records'], - 'Line' => $data['line'], - 'TTL' => $data['ttl'], - 'MX' => isset($data['mx']) ? $data['mx'] : null, - 'Status' => $data['status'] == 'ACTIVE' ? '1' : '0', - 'Weight' => $data['weight'], - 'Remark' => $data['description'], - 'UpdateTime' => $data['updated_at'], - ]; - } - return false; - } - - //添加解析记录 - public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $Name = $this->getHost($Name); - if ($Type == 'TXT' && substr($Value, 0, 1) != '"') $Value = '"' . $Value . '"'; - $records = array_reverse(explode(',', $Value)); - $params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => $records, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark]; - if ($Type == 'MX') $params['records'][0] = intval($MX) . ' ' . $Value; - if ($Weight > 0) $params['weight'] = intval($Weight); - $data = $this->send_request('POST', '/v2.1/zones/'.$this->domainid.'/recordsets', null, $params); - return is_array($data) ? $data['id'] : false; - } - - //修改解析记录 - public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $Name = $this->getHost($Name); - if ($Type == 'TXT' && substr($Value, 0, 1) != '"') $Value = '"' . $Value . '"'; - $records = array_reverse(explode(',', $Value)); - $params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => $records, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark]; - if ($Type == 'MX') $params['records'][0] = intval($MX) . ' ' . $Value; - if ($Weight > 0) $params['weight'] = intval($Weight); - $data = $this->send_request('PUT', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId, null, $params); - return is_array($data); - } - - //修改解析记录备注 - public function updateDomainRecordRemark($RecordId, $Remark) - { - return false; - } - - //删除解析记录 - public function deleteDomainRecord($RecordId) - { - $data = $this->send_request('DELETE', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId); - return is_array($data); - } - - //设置解析记录状态 - public function setDomainRecordStatus($RecordId, $Status) - { - $Status = $Status == '1' ? 'ENABLE' : 'DISABLE'; - $params = ['status' => $Status]; - $data = $this->send_request('PUT', '/v2.1/recordsets/'.$RecordId.'/statuses/set', null, $params); - return is_array($data); - } - - //获取解析记录操作日志 - public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) - { - return false; - } - - //获取解析线路列表 - public function getRecordLine() - { - $file_path = app()->getBasePath().'data'.DIRECTORY_SEPARATOR.'huawei_line.json'; - $content = file_get_contents($file_path); - $data = json_decode($content, true); - if ($data) { - return $data; - $list = [$data['DEFAULT']['id'] => ['name' => $data['DEFAULT']['zh'], 'parent' => null]]; - $this->processLineList($list, $data['ISP'], null, 1, 1); - $this->processLineList($list, $data['REGION'], null, null, 1); - //file_put_contents($file_path, json_encode($list, JSON_UNESCAPED_UNICODE)); - return $list; - } - return false; - } - - private function processLineList(&$list, $line_list, $parent, $rootId = null, $rootName = null) - { - foreach ($line_list as $row) { - if ($rootId && $rootId !== 1) { - $row['id'] = $rootId.'_'.$row['id']; - } - if ($rootName && $rootName !== 1) { - $row['zh'] = $rootName.'_'.$row['zh']; - } - $list[$row['id']] = ['name' => $row['zh'], 'parent' => $parent]; - if (isset($row['children']) && !empty($row['children'])) { - $this->processLineList($list, $row['children'], $row['id'], $rootId === 1 ? $row['id'] : $rootId, $rootName === 1 ? $row['zh'] : $rootName); - } - } - } - - //获取域名概览信息 - public function getDomainInfo() - { - return $this->send_request('GET', '/v2/zones/'.$this->domainid); - } - - //获取域名最低TTL - public function getMinTTL() - { - return false; - } - - public function addDomain($Domain) - { - $params = [ - 'name' => $Domain, - ]; - $data = $this->send_request('POST', '/v2/zones', null, $params); - if ($data) { - return ['id' => $data['id'], 'name' => rtrim($data['name'], '.')]; - } - return false; - } - - private function convertType($type) - { - return $type; - } - - private function getHost($Name) - { - if ($Name == '@') $Name = ''; - else $Name .= '.'; - $Name .= $this->domain . '.'; - return $Name; - } - - private function send_request($method, $path, $query = null, $params = null) - { - try{ - return $this->client->request($method, $path, $query, $params); - }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); - } -} +AccessKeyId = $config['ak']; + $this->SecretAccessKey = $config['sk']; + $proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + $this->client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $proxy); + $this->domain = $config['domain']; + $this->domainid = $config['domainid']; + } + + public function getError() + { + return $this->error; + } + + public function check() + { + if ($this->getDomainList() != false) { + return true; + } + return false; + } + + //获取域名列表 + public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20) + { + $offset = ($PageNumber - 1) * $PageSize; + $query = ['offset' => $offset, 'limit' => $PageSize, 'name' => $KeyWord]; + $data = $this->send_request('GET', '/v2/zones', $query); + if ($data) { + $list = []; + foreach ($data['zones'] as $row) { + $list[] = [ + 'DomainId' => $row['id'], + 'Domain' => rtrim($row['name'], '.'), + 'RecordCount' => $row['record_num'], + ]; + } + return ['total' => $data['metadata']['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) + { + $offset = ($PageNumber - 1) * $PageSize; + $query = ['type' => $Type, 'line_id' => $Line, 'name' => $KeyWord, 'offset' => $offset, 'limit' => $PageSize]; + if (!isNullOrEmpty($Status)) { + $Status = $Status == '1' ? 'ACTIVE' : 'DISABLE'; + $query['status'] = $Status; + } + if (!isNullOrEmpty($SubDomain)) { + $SubDomain = $this->getHost($SubDomain); + $query['name'] = $SubDomain; + $query['search_mode'] = 'equal'; + } + $data = $this->send_request('GET', '/v2.1/zones/'.$this->domainid.'/recordsets', $query); + if ($data) { + $list = []; + foreach ($data['recordsets'] as $row) { + if ($row['name'] == $row['zone_name']) $row['name'] = '@'; + if ($row['type'] == 'MX') list($row['mx'], $row['records']) = explode(' ', $row['records'][0]); + $list[] = [ + 'RecordId' => $row['id'], + 'Domain' => rtrim($row['zone_name'], '.'), + 'Name' => str_replace('.'.$row['zone_name'], '', $row['name']), + 'Type' => $row['type'], + 'Value' => $row['records'], + 'Line' => $row['line'], + 'TTL' => $row['ttl'], + 'MX' => isset($row['mx']) ? $row['mx'] : null, + 'Status' => $row['status'] == 'ACTIVE' ? '1' : '0', + 'Weight' => $row['weight'], + 'Remark' => $row['description'], + 'UpdateTime' => $row['updated_at'], + ]; + } + return ['total' => $data['metadata']['total_count'], '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) + { + $data = $this->send_request('GET', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId); + if ($data) { + if ($data['name'] == $data['zone_name']) $data['name'] = '@'; + if ($data['type'] == 'MX') list($data['mx'], $data['records']) = explode(' ', $data['records'][0]); + return [ + 'RecordId' => $data['id'], + 'Domain' => rtrim($data['zone_name'], '.'), + 'Name' => str_replace('.'.$data['zone_name'], '', $data['name']), + 'Type' => $data['type'], + 'Value' => $data['records'], + 'Line' => $data['line'], + 'TTL' => $data['ttl'], + 'MX' => isset($data['mx']) ? $data['mx'] : null, + 'Status' => $data['status'] == 'ACTIVE' ? '1' : '0', + 'Weight' => $data['weight'], + 'Remark' => $data['description'], + 'UpdateTime' => $data['updated_at'], + ]; + } + return false; + } + + //添加解析记录 + public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $Name = $this->getHost($Name); + if ($Type == 'TXT' && substr($Value, 0, 1) != '"') $Value = '"' . $Value . '"'; + $records = array_reverse(explode(',', $Value)); + $params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => $records, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark]; + if ($Type == 'MX') $params['records'][0] = intval($MX) . ' ' . $Value; + if ($Weight > 0) $params['weight'] = intval($Weight); + $data = $this->send_request('POST', '/v2.1/zones/'.$this->domainid.'/recordsets', null, $params); + return is_array($data) ? $data['id'] : false; + } + + //修改解析记录 + public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $Name = $this->getHost($Name); + if ($Type == 'TXT' && substr($Value, 0, 1) != '"') $Value = '"' . $Value . '"'; + $records = array_reverse(explode(',', $Value)); + $params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => $records, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark]; + if ($Type == 'MX') $params['records'][0] = intval($MX) . ' ' . $Value; + if ($Weight > 0) $params['weight'] = intval($Weight); + $data = $this->send_request('PUT', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId, null, $params); + return is_array($data); + } + + //修改解析记录备注 + public function updateDomainRecordRemark($RecordId, $Remark) + { + return false; + } + + //删除解析记录 + public function deleteDomainRecord($RecordId) + { + $data = $this->send_request('DELETE', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId); + return is_array($data); + } + + //设置解析记录状态 + public function setDomainRecordStatus($RecordId, $Status) + { + $Status = $Status == '1' ? 'ENABLE' : 'DISABLE'; + $params = ['status' => $Status]; + $data = $this->send_request('PUT', '/v2.1/recordsets/'.$RecordId.'/statuses/set', null, $params); + return is_array($data); + } + + //获取解析记录操作日志 + public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) + { + return false; + } + + //获取解析线路列表 + public function getRecordLine() + { + $file_path = app()->getBasePath().'data'.DIRECTORY_SEPARATOR.'huawei_line.json'; + $content = file_get_contents($file_path); + $data = json_decode($content, true); + if ($data) { + return $data; + $list = [$data['DEFAULT']['id'] => ['name' => $data['DEFAULT']['zh'], 'parent' => null]]; + $this->processLineList($list, $data['ISP'], null, 1, 1); + $this->processLineList($list, $data['REGION'], null, null, 1); + //file_put_contents($file_path, json_encode($list, JSON_UNESCAPED_UNICODE)); + return $list; + } + return false; + } + + private function processLineList(&$list, $line_list, $parent, $rootId = null, $rootName = null) + { + foreach ($line_list as $row) { + if ($rootId && $rootId !== 1) { + $row['id'] = $rootId.'_'.$row['id']; + } + if ($rootName && $rootName !== 1) { + $row['zh'] = $rootName.'_'.$row['zh']; + } + $list[$row['id']] = ['name' => $row['zh'], 'parent' => $parent]; + if (isset($row['children']) && !empty($row['children'])) { + $this->processLineList($list, $row['children'], $row['id'], $rootId === 1 ? $row['id'] : $rootId, $rootName === 1 ? $row['zh'] : $rootName); + } + } + } + + //获取域名概览信息 + public function getDomainInfo() + { + return $this->send_request('GET', '/v2/zones/'.$this->domainid); + } + + //获取域名最低TTL + public function getMinTTL() + { + return false; + } + + public function addDomain($Domain) + { + $params = [ + 'name' => $Domain, + ]; + $data = $this->send_request('POST', '/v2/zones', null, $params); + if ($data) { + return ['id' => $data['id'], 'name' => rtrim($data['name'], '.')]; + } + return false; + } + + private function convertType($type) + { + return $type; + } + + private function getHost($Name) + { + if ($Name == '@') $Name = ''; + else $Name .= '.'; + $Name .= $this->domain . '.'; + return $Name; + } + + private function send_request($method, $path, $query = null, $params = null) + { + try{ + return $this->client->request($method, $path, $query, $params); + }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); + } +} diff --git a/app/lib/dns/huoshan.php b/app/lib/dns/huoshan.php index 355fe20..4b93136 100644 --- a/app/lib/dns/huoshan.php +++ b/app/lib/dns/huoshan.php @@ -1,280 +1,280 @@ - ['level' => 1, 'name' => '免费版', 'ttl' => 600], - 'professional_inner' => ['level' => 2, 'name' => '专业版', 'ttl' => 300], - 'enterprise_inner' => ['level' => 3, 'name' => '企业版', 'ttl' => 60], - 'ultimate_inner' => ['level' => 4, 'name' => '旗舰版', 'ttl' => 1], - 'ultimate_exclusive_inner' => ['level' => 5, 'name' => '尊享版', 'ttl' => 1], - ]; - - public function __construct($config) - { - $this->AccessKeyId = $config['ak']; - $this->SecretAccessKey = $config['sk']; - $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']; - $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) - { - $query = ['PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'Key' => $KeyWord]; - $data = $this->send_request('GET', 'ListZones', $query); - if ($data) { - $list = []; - if (!empty($data['Zones'])) { - foreach ($data['Zones'] as $row) { - $list[] = [ - 'DomainId' => $row['ZID'], - 'Domain' => $row['ZoneName'], - 'RecordCount' => $row['RecordCount'], - ]; - } - } - return ['total' => $data['Total'], 'list' => $list]; - } - return false; - } - - //获取解析记录列表 - public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null) - { - $query = ['ZID' => intval($this->domainid), 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'SearchOrder' => 'desc']; - if (!empty($SubDomain) || !empty($Type) || !empty($Line) || !empty($Value)) { - $query += ['Host' => $SubDomain, 'Value' => $Value, 'Type' => $Type, 'Line' => $Line, 'SearchMode' => 'exact']; - } elseif (!empty($KeyWord)) { - $query += ['Host' => $KeyWord]; - } - $data = $this->send_request('GET', 'ListRecords', $query); - if ($data) { - $list = []; - foreach ($data['Records'] as $row) { - if ($row['Type'] == 'MX') list($row['MX'], $row['Value']) = explode(' ', $row['Value']); - $list[] = [ - 'RecordId' => $row['RecordID'], - 'Domain' => $this->domain, - 'Name' => $row['Host'], - 'Type' => $row['Type'], - 'Value' => $row['Value'], - 'Line' => $row['Line'], - 'TTL' => $row['TTL'], - 'MX' => isset($row['MX']) ? $row['MX'] : null, - 'Status' => $row['Enable'] ? '1' : '0', - 'Weight' => $row['Weight'], - 'Remark' => $row['Remark'], - 'UpdateTime' => $row['UpdatedAt'], - ]; - } - 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) - { - $data = $this->send_request('GET', 'QueryRecord', ['RecordID' => $RecordId]); - if ($data) { - if ($data['name'] == $data['zone_name']) $data['name'] = '@'; - if ($data['Type'] == 'MX') list($data['MX'], $data['Value']) = explode(' ', $data['Value']); - return [ - 'RecordId' => $data['RecordID'], - 'Domain' => $this->domain, - 'Name' => $data['Host'], - 'Type' => $data['Type'], - 'Value' => $data['Value'], - 'Line' => $data['Line'], - 'TTL' => $data['TTL'], - 'MX' => isset($data['MX']) ? $data['MX'] : null, - 'Status' => $data['Enable'] ? '1' : '0', - 'Weight' => $data['Weight'], - 'Remark' => $data['Remark'], - 'UpdateTime' => $data['UpdatedAt'], - ]; - } - return false; - } - - //添加解析记录 - public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $params = ['ZID' => intval($this->domainid), 'Host' => $Name, 'Type' => $this->convertType($Type), 'Value' => $Value, 'Line' => $Line, 'TTL' => intval($TTL), 'Remark' => $Remark]; - if ($Type == 'MX') $params['Value'] = intval($MX) . ' ' . $Value; - if ($Weight > 0) $params['Weight'] = $Weight; - $data = $this->send_request('POST', 'CreateRecord', $params); - return is_array($data) ? $data['RecordID'] : false; - } - - //修改解析记录 - public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $params = ['RecordID' => $RecordId, 'Host' => $Name, 'Type' => $this->convertType($Type), 'Value' => $Value, 'Line' => $Line, 'TTL' => intval($TTL), 'Remark' => $Remark]; - if ($Type == 'MX') $params['Value'] = intval($MX) . ' ' . $Value; - if ($Weight > 0) $params['Weight'] = $Weight; - $data = $this->send_request('POST', 'UpdateRecord', $params); - return is_array($data); - } - - //修改解析记录备注 - public function updateDomainRecordRemark($RecordId, $Remark) - { - return false; - } - - //删除解析记录 - public function deleteDomainRecord($RecordId) - { - $data = $this->send_request('POST', 'DeleteRecord', ['RecordID' => $RecordId]); - return $data; - } - - //设置解析记录状态 - public function setDomainRecordStatus($RecordId, $Status) - { - $params = ['RecordID' => $RecordId, 'Enable' => $Status == '1']; - $data = $this->send_request('POST', 'UpdateRecordStatus', $params); - return is_array($data); - } - - //获取解析记录操作日志 - public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) - { - return false; - } - - //获取解析线路列表 - public function getRecordLine() - { - $domainInfo = $this->getDomainInfo(); - if (!$domainInfo) return false; - $level = $this->getTradeInfo($domainInfo['TradeCode'])['level']; - $data = $this->send_request('GET', 'ListLines', []); - if ($data) { - $list = []; - $list['default'] = ['name' => '默认', 'parent' => null]; - foreach ($data['Lines'] as $row) { - if ($row['Value'] == 'default') continue; - if ($row['Level'] > $level) continue; - $list[$row['Value']] = ['name' => $row['Name'], 'parent' => isset($row['FatherValue']) ? $row['FatherValue'] : null]; - } - - $data = $this->send_request('GET', 'ListCustomLines', []); - if ($data && $data['TotalCount'] > 0) { - $list['N.customer_lines'] = ['name' => '自定义线路', 'parent' => null]; - foreach ($data['CustomerLines'] as $row) { - $list[$row['Line']] = ['name' => $row['NameCN'], 'parent' => 'N.customer_lines']; - } - } - - return $list; - } - return false; - } - - //获取域名概览信息 - public function getDomainInfo() - { - if (!empty($this->domainInfo)) return $this->domainInfo; - $query = ['ZID' => intval($this->domainid)]; - $data = $this->send_request('GET', 'QueryZone', $query); - if ($data) { - $this->domainInfo = $data; - return $data; - } - return false; - } - - //获取域名最低TTL - public function getMinTTL() - { - $domainInfo = $this->getDomainInfo(); - if ($domainInfo) { - $ttl = $this->getTradeInfo($domainInfo['TradeCode'])['ttl']; - return $ttl; - } - return false; - } - - public function addDomain($Domain) - { - $params = ['ZoneName' => $Domain]; - $data = $this->send_request('POST', 'CreateZone', $params); - if ($data) { - return ['id' => $data['ZID'], 'name' => $data['ZoneName']]; - } - return false; - } - - private function convertType($type) - { - return $type; - } - - private function getTradeInfo($trade_code) - { - if (array_key_exists($trade_code, self::$trade_code_list)) { - $trade_code = $trade_code; - } else { - $trade_code = 'free_inner'; - } - return self::$trade_code_list[$trade_code]; - } - - private function send_request($method, $action, $params = []) - { - try{ - return $this->client->request($method, $action, $params); - }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); - } -} + ['level' => 1, 'name' => '免费版', 'ttl' => 600], + 'professional_inner' => ['level' => 2, 'name' => '专业版', 'ttl' => 300], + 'enterprise_inner' => ['level' => 3, 'name' => '企业版', 'ttl' => 60], + 'ultimate_inner' => ['level' => 4, 'name' => '旗舰版', 'ttl' => 1], + 'ultimate_exclusive_inner' => ['level' => 5, 'name' => '尊享版', 'ttl' => 1], + ]; + + public function __construct($config) + { + $this->AccessKeyId = $config['ak']; + $this->SecretAccessKey = $config['sk']; + $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']; + $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) + { + $query = ['PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'Key' => $KeyWord]; + $data = $this->send_request('GET', 'ListZones', $query); + if ($data) { + $list = []; + if (!empty($data['Zones'])) { + foreach ($data['Zones'] as $row) { + $list[] = [ + 'DomainId' => $row['ZID'], + 'Domain' => $row['ZoneName'], + 'RecordCount' => $row['RecordCount'], + ]; + } + } + return ['total' => $data['Total'], 'list' => $list]; + } + return false; + } + + //获取解析记录列表 + public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null) + { + $query = ['ZID' => intval($this->domainid), 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'SearchOrder' => 'desc']; + if (!empty($SubDomain) || !empty($Type) || !empty($Line) || !empty($Value)) { + $query += ['Host' => $SubDomain, 'Value' => $Value, 'Type' => $Type, 'Line' => $Line, 'SearchMode' => 'exact']; + } elseif (!empty($KeyWord)) { + $query += ['Host' => $KeyWord]; + } + $data = $this->send_request('GET', 'ListRecords', $query); + if ($data) { + $list = []; + foreach ($data['Records'] as $row) { + if ($row['Type'] == 'MX') list($row['MX'], $row['Value']) = explode(' ', $row['Value']); + $list[] = [ + 'RecordId' => $row['RecordID'], + 'Domain' => $this->domain, + 'Name' => $row['Host'], + 'Type' => $row['Type'], + 'Value' => $row['Value'], + 'Line' => $row['Line'], + 'TTL' => $row['TTL'], + 'MX' => isset($row['MX']) ? $row['MX'] : null, + 'Status' => $row['Enable'] ? '1' : '0', + 'Weight' => $row['Weight'], + 'Remark' => $row['Remark'], + 'UpdateTime' => $row['UpdatedAt'], + ]; + } + 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) + { + $data = $this->send_request('GET', 'QueryRecord', ['RecordID' => $RecordId]); + if ($data) { + if ($data['name'] == $data['zone_name']) $data['name'] = '@'; + if ($data['Type'] == 'MX') list($data['MX'], $data['Value']) = explode(' ', $data['Value']); + return [ + 'RecordId' => $data['RecordID'], + 'Domain' => $this->domain, + 'Name' => $data['Host'], + 'Type' => $data['Type'], + 'Value' => $data['Value'], + 'Line' => $data['Line'], + 'TTL' => $data['TTL'], + 'MX' => isset($data['MX']) ? $data['MX'] : null, + 'Status' => $data['Enable'] ? '1' : '0', + 'Weight' => $data['Weight'], + 'Remark' => $data['Remark'], + 'UpdateTime' => $data['UpdatedAt'], + ]; + } + return false; + } + + //添加解析记录 + public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $params = ['ZID' => intval($this->domainid), 'Host' => $Name, 'Type' => $this->convertType($Type), 'Value' => $Value, 'Line' => $Line, 'TTL' => intval($TTL), 'Remark' => $Remark]; + if ($Type == 'MX') $params['Value'] = intval($MX) . ' ' . $Value; + if ($Weight > 0) $params['Weight'] = $Weight; + $data = $this->send_request('POST', 'CreateRecord', $params); + return is_array($data) ? $data['RecordID'] : false; + } + + //修改解析记录 + public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $params = ['RecordID' => $RecordId, 'Host' => $Name, 'Type' => $this->convertType($Type), 'Value' => $Value, 'Line' => $Line, 'TTL' => intval($TTL), 'Remark' => $Remark]; + if ($Type == 'MX') $params['Value'] = intval($MX) . ' ' . $Value; + if ($Weight > 0) $params['Weight'] = $Weight; + $data = $this->send_request('POST', 'UpdateRecord', $params); + return is_array($data); + } + + //修改解析记录备注 + public function updateDomainRecordRemark($RecordId, $Remark) + { + return false; + } + + //删除解析记录 + public function deleteDomainRecord($RecordId) + { + $data = $this->send_request('POST', 'DeleteRecord', ['RecordID' => $RecordId]); + return $data; + } + + //设置解析记录状态 + public function setDomainRecordStatus($RecordId, $Status) + { + $params = ['RecordID' => $RecordId, 'Enable' => $Status == '1']; + $data = $this->send_request('POST', 'UpdateRecordStatus', $params); + return is_array($data); + } + + //获取解析记录操作日志 + public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) + { + return false; + } + + //获取解析线路列表 + public function getRecordLine() + { + $domainInfo = $this->getDomainInfo(); + if (!$domainInfo) return false; + $level = $this->getTradeInfo($domainInfo['TradeCode'])['level']; + $data = $this->send_request('GET', 'ListLines', []); + if ($data) { + $list = []; + $list['default'] = ['name' => '默认', 'parent' => null]; + foreach ($data['Lines'] as $row) { + if ($row['Value'] == 'default') continue; + if ($row['Level'] > $level) continue; + $list[$row['Value']] = ['name' => $row['Name'], 'parent' => isset($row['FatherValue']) ? $row['FatherValue'] : null]; + } + + $data = $this->send_request('GET', 'ListCustomLines', []); + if ($data && $data['TotalCount'] > 0) { + $list['N.customer_lines'] = ['name' => '自定义线路', 'parent' => null]; + foreach ($data['CustomerLines'] as $row) { + $list[$row['Line']] = ['name' => $row['NameCN'], 'parent' => 'N.customer_lines']; + } + } + + return $list; + } + return false; + } + + //获取域名概览信息 + public function getDomainInfo() + { + if (!empty($this->domainInfo)) return $this->domainInfo; + $query = ['ZID' => intval($this->domainid)]; + $data = $this->send_request('GET', 'QueryZone', $query); + if ($data) { + $this->domainInfo = $data; + return $data; + } + return false; + } + + //获取域名最低TTL + public function getMinTTL() + { + $domainInfo = $this->getDomainInfo(); + if ($domainInfo) { + $ttl = $this->getTradeInfo($domainInfo['TradeCode'])['ttl']; + return $ttl; + } + return false; + } + + public function addDomain($Domain) + { + $params = ['ZoneName' => $Domain]; + $data = $this->send_request('POST', 'CreateZone', $params); + if ($data) { + return ['id' => $data['ZID'], 'name' => $data['ZoneName']]; + } + return false; + } + + private function convertType($type) + { + return $type; + } + + private function getTradeInfo($trade_code) + { + if (array_key_exists($trade_code, self::$trade_code_list)) { + $trade_code = $trade_code; + } else { + $trade_code = 'free_inner'; + } + return self::$trade_code_list[$trade_code]; + } + + private function send_request($method, $action, $params = []) + { + try{ + return $this->client->request($method, $action, $params); + }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); + } +} diff --git a/app/lib/dns/jdcloud.php b/app/lib/dns/jdcloud.php index 8767237..e69ddb1 100644 --- a/app/lib/dns/jdcloud.php +++ b/app/lib/dns/jdcloud.php @@ -1,263 +1,263 @@ -AccessKeyId = $config['ak']; - $this->AccessKeySecret = $config['sk']; - $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']; - $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) - { - $query = ['pageNumber' => $PageNumber, 'pageSize' => $PageSize, 'domainName' => $KeyWord]; - $data = $this->send_request('GET', '/domain', $query); - if ($data) { - $list = []; - if (!empty($data['dataList'])) { - foreach ($data['dataList'] as $row) { - $list[] = [ - 'DomainId' => $row['id'], - 'Domain' => $row['domainName'], - '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) - { - if ($PageSize > 99) $PageSize = 99; - $query = ['pageNumber' => $PageNumber, 'pageSize' => $PageSize]; - if (!isNullOrEmpty($SubDomain)) { - $SubDomain = strtolower($SubDomain); - $query += ['search' => $SubDomain]; - } elseif (!isNullOrEmpty($KeyWord)) { - $query += ['search' => $KeyWord]; - } - $data = $this->send_request('GET', '/domain/'.$this->domainid.'/ResourceRecord', $query); - if ($data) { - $list = []; - foreach ($data['dataList'] as $row) { - if ($row['type'] == 'SRV') { - $row['hostValue'] = $row['mxPriority'].' '.$row['weight'].' '.$row['port'].' '.$row['hostValue']; - } - $list[] = [ - 'RecordId' => $row['id'], - 'Domain' => $this->domain, - 'Name' => $row['hostRecord'], - 'Type' => $row['type'], - 'Value' => $row['hostValue'], - 'Line' => array_pop($row['viewValue']), - 'TTL' => $row['ttl'], - 'MX' => isset($row['mxPriority']) ? $row['mxPriority'] : null, - 'Status' => $row['resolvingStatus'] == '2' ? '1' : '0', - 'Weight' => $row['weight'], - 'Remark' => null, - 'UpdateTime' => date('Y-m-d H:i:s', $row['updateTime']), - ]; - } - if (!isNullOrEmpty($SubDomain) && !empty($list)) { - $list = array_values(array_filter($list, function ($v) use ($SubDomain) { - return $v['Name'] == $SubDomain; - })); - } - 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) - { - return false; - } - - //添加解析记录 - public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $params = ['hostRecord' => $Name, 'type' => $this->convertType($Type), 'hostValue' => $Value, 'viewValue' => intval($Line), 'ttl' => intval($TTL)]; - if ($Type == 'MX') $params['mxPriority'] = intval($MX); - if (!isNullOrEmpty($Weight)) $params['weight'] = intval($Weight); - if ($Type == 'SRV') { - $values = explode(' ', $Value); - $params['mxPriority'] = intval($values[0]); - $params['weight'] = intval($values[1]); - $params['port'] = intval($values[2]); - $params['hostValue'] = $values[3]; - } - $data = $this->send_request('POST', '/domain/'.$this->domainid.'/ResourceRecord', ['req'=>$params]); - return is_array($data) ? $data['dataList']['id'] : false; - } - - //修改解析记录 - public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $params = ['domainName'=>$this->domain, 'hostRecord' => $Name, 'type' => $this->convertType($Type), 'hostValue' => $Value, 'viewValue' => intval($Line), 'ttl' => intval($TTL)]; - if ($Type == 'MX') $params['mxPriority'] = intval($MX); - if (!isNullOrEmpty($Weight)) $params['weight'] = intval($Weight); - if ($Type == 'SRV') { - $values = explode(' ', $Value); - $params['mxPriority'] = intval($values[0]); - $params['weight'] = intval($values[1]); - $params['port'] = intval($values[2]); - $params['hostValue'] = $values[3]; - } - return $this->send_request('PUT', '/domain/'.$this->domainid.'/ResourceRecord/'.$RecordId, ['req'=>$params]); - } - - //修改解析记录备注 - public function updateDomainRecordRemark($RecordId, $Remark) - { - return false; - } - - //删除解析记录 - public function deleteDomainRecord($RecordId) - { - return $this->send_request('DELETE', '/domain/'.$this->domainid.'/ResourceRecord/'.$RecordId); - } - - //设置解析记录状态 - public function setDomainRecordStatus($RecordId, $Status) - { - $params = ['action' => $Status == '1' ? 'enable' : 'disable']; - $data = $this->send_request('PUT', '/domain/'.$this->domainid.'/ResourceRecord/'.$RecordId.'/status', $params); - return is_array($data); - } - - //获取解析记录操作日志 - public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) - { - return false; - } - - //获取解析线路列表 - public function getRecordLine() - { - $domainInfo = $this->getDomainInfo(); - if (!$domainInfo) return false; - $packId = $domainInfo['packId']; - $data = $this->send_request('GET', '/domain/'.$this->domainid.'/viewTree', ['packId'=>$packId, 'viewId'=>'0']); - if ($data) { - $list = []; - $this->processLineList($list, $data['data'], null); - return $list; - } - return false; - } - - private function processLineList(&$list, $line_list, $parent) - { - foreach ($line_list as $row) { - if ($row['disabled']) continue; - if (!isset($list[$row['value']])) { - $list[$row['value']] = ['name' => $row['label'], 'parent' => $parent]; - if (!$row['leaf'] && $row['children']) { - $this->processLineList($list, $row['children'], $row['value']); - } - } - } - } - - //获取域名概览信息 - public function getDomainInfo() - { - if (!empty($this->domainInfo)) return $this->domainInfo; - $query = ['domainId' => intval($this->domainid)]; - $data = $this->send_request('GET', '/domain', $query); - if ($data && $data['dataList']) { - return $data['dataList'][0]; - } - return false; - } - - //获取域名最低TTL - public function getMinTTL() - { - return false; - } - - public function addDomain($Domain) - { - $params = ['packId' => 0, 'domainName' => $Domain]; - $data = $this->send_request('POST', '/domain', $params); - if ($data) { - return ['id' => $data['data']['id'], 'name' => $data['data']['domainName']]; - } - return false; - } - - private function convertType($type) - { - $convert_dict = ['REDIRECT_URL' => 'EXPLICIT_URL', 'FORWARD_URL' => 'IMPLICIT_URL']; - if (array_key_exists($type, $convert_dict)) { - return $convert_dict[$type]; - } - return $type; - } - - private function send_request($method, $action, $params = []) - { - $path = '/'.$this->version.'/regions/'.$this->region.$action; - try{ - return $this->client->request($method, $path, $params); - }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); - } -} +AccessKeyId = $config['ak']; + $this->AccessKeySecret = $config['sk']; + $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']; + $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) + { + $query = ['pageNumber' => $PageNumber, 'pageSize' => $PageSize, 'domainName' => $KeyWord]; + $data = $this->send_request('GET', '/domain', $query); + if ($data) { + $list = []; + if (!empty($data['dataList'])) { + foreach ($data['dataList'] as $row) { + $list[] = [ + 'DomainId' => $row['id'], + 'Domain' => $row['domainName'], + '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) + { + if ($PageSize > 99) $PageSize = 99; + $query = ['pageNumber' => $PageNumber, 'pageSize' => $PageSize]; + if (!isNullOrEmpty($SubDomain)) { + $SubDomain = strtolower($SubDomain); + $query += ['search' => $SubDomain]; + } elseif (!isNullOrEmpty($KeyWord)) { + $query += ['search' => $KeyWord]; + } + $data = $this->send_request('GET', '/domain/'.$this->domainid.'/ResourceRecord', $query); + if ($data) { + $list = []; + foreach ($data['dataList'] as $row) { + if ($row['type'] == 'SRV') { + $row['hostValue'] = $row['mxPriority'].' '.$row['weight'].' '.$row['port'].' '.$row['hostValue']; + } + $list[] = [ + 'RecordId' => $row['id'], + 'Domain' => $this->domain, + 'Name' => $row['hostRecord'], + 'Type' => $row['type'], + 'Value' => $row['hostValue'], + 'Line' => array_pop($row['viewValue']), + 'TTL' => $row['ttl'], + 'MX' => isset($row['mxPriority']) ? $row['mxPriority'] : null, + 'Status' => $row['resolvingStatus'] == '2' ? '1' : '0', + 'Weight' => $row['weight'], + 'Remark' => null, + 'UpdateTime' => date('Y-m-d H:i:s', $row['updateTime']), + ]; + } + if (!isNullOrEmpty($SubDomain) && !empty($list)) { + $list = array_values(array_filter($list, function ($v) use ($SubDomain) { + return $v['Name'] == $SubDomain; + })); + } + 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) + { + return false; + } + + //添加解析记录 + public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $params = ['hostRecord' => $Name, 'type' => $this->convertType($Type), 'hostValue' => $Value, 'viewValue' => intval($Line), 'ttl' => intval($TTL)]; + if ($Type == 'MX') $params['mxPriority'] = intval($MX); + if (!isNullOrEmpty($Weight)) $params['weight'] = intval($Weight); + if ($Type == 'SRV') { + $values = explode(' ', $Value); + $params['mxPriority'] = intval($values[0]); + $params['weight'] = intval($values[1]); + $params['port'] = intval($values[2]); + $params['hostValue'] = $values[3]; + } + $data = $this->send_request('POST', '/domain/'.$this->domainid.'/ResourceRecord', ['req'=>$params]); + return is_array($data) ? $data['dataList']['id'] : false; + } + + //修改解析记录 + public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $params = ['domainName'=>$this->domain, 'hostRecord' => $Name, 'type' => $this->convertType($Type), 'hostValue' => $Value, 'viewValue' => intval($Line), 'ttl' => intval($TTL)]; + if ($Type == 'MX') $params['mxPriority'] = intval($MX); + if (!isNullOrEmpty($Weight)) $params['weight'] = intval($Weight); + if ($Type == 'SRV') { + $values = explode(' ', $Value); + $params['mxPriority'] = intval($values[0]); + $params['weight'] = intval($values[1]); + $params['port'] = intval($values[2]); + $params['hostValue'] = $values[3]; + } + return $this->send_request('PUT', '/domain/'.$this->domainid.'/ResourceRecord/'.$RecordId, ['req'=>$params]); + } + + //修改解析记录备注 + public function updateDomainRecordRemark($RecordId, $Remark) + { + return false; + } + + //删除解析记录 + public function deleteDomainRecord($RecordId) + { + return $this->send_request('DELETE', '/domain/'.$this->domainid.'/ResourceRecord/'.$RecordId); + } + + //设置解析记录状态 + public function setDomainRecordStatus($RecordId, $Status) + { + $params = ['action' => $Status == '1' ? 'enable' : 'disable']; + $data = $this->send_request('PUT', '/domain/'.$this->domainid.'/ResourceRecord/'.$RecordId.'/status', $params); + return is_array($data); + } + + //获取解析记录操作日志 + public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) + { + return false; + } + + //获取解析线路列表 + public function getRecordLine() + { + $domainInfo = $this->getDomainInfo(); + if (!$domainInfo) return false; + $packId = $domainInfo['packId']; + $data = $this->send_request('GET', '/domain/'.$this->domainid.'/viewTree', ['packId'=>$packId, 'viewId'=>'0']); + if ($data) { + $list = []; + $this->processLineList($list, $data['data'], null); + return $list; + } + return false; + } + + private function processLineList(&$list, $line_list, $parent) + { + foreach ($line_list as $row) { + if ($row['disabled']) continue; + if (!isset($list[$row['value']])) { + $list[$row['value']] = ['name' => $row['label'], 'parent' => $parent]; + if (!$row['leaf'] && $row['children']) { + $this->processLineList($list, $row['children'], $row['value']); + } + } + } + } + + //获取域名概览信息 + public function getDomainInfo() + { + if (!empty($this->domainInfo)) return $this->domainInfo; + $query = ['domainId' => intval($this->domainid)]; + $data = $this->send_request('GET', '/domain', $query); + if ($data && $data['dataList']) { + return $data['dataList'][0]; + } + return false; + } + + //获取域名最低TTL + public function getMinTTL() + { + return false; + } + + public function addDomain($Domain) + { + $params = ['packId' => 0, 'domainName' => $Domain]; + $data = $this->send_request('POST', '/domain', $params); + if ($data) { + return ['id' => $data['data']['id'], 'name' => $data['data']['domainName']]; + } + return false; + } + + private function convertType($type) + { + $convert_dict = ['REDIRECT_URL' => 'EXPLICIT_URL', 'FORWARD_URL' => 'IMPLICIT_URL']; + if (array_key_exists($type, $convert_dict)) { + return $convert_dict[$type]; + } + return $type; + } + + private function send_request($method, $action, $params = []) + { + $path = '/'.$this->version.'/regions/'.$this->region.$action; + try{ + return $this->client->request($method, $path, $params); + }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); + } +} diff --git a/app/lib/dns/namesilo.php b/app/lib/dns/namesilo.php index 58f3844..d852c5e 100644 --- a/app/lib/dns/namesilo.php +++ b/app/lib/dns/namesilo.php @@ -1,230 +1,230 @@ -apikey = $config['sk']; - $this->domain = $config['domain']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - } - - public function getError() - { - return $this->error; - } - - public function check() - { - if ($this->getDomainList() !== false) { - return true; - } - return false; - } - - //获取域名列表 - public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20) - { - $param = ['page' => $PageNumber, 'pageSize' => $PageSize]; - $data = $this->send_reuqest('listDomains', $param); - if ($data) { - $list = []; - if($data['domains']){ - foreach ($data['domains'] as $row) { - $list[] = [ - 'DomainId' => $row['domain'], - 'Domain' => $row['domain'], - 'RecordCount' => 0, - ]; - } - } - return ['total' => $data['pager']['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' => $this->domain]; - $data = $this->send_reuqest('dnsListRecords', $param); - 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, - 'Type' => $row['type'], - 'Value' => $row['value'], - 'Line' => 'default', - 'TTL' => $row['ttl'], - 'MX' => isset($row['distance']) ? $row['distance'] : null, - 'Status' => '1', - 'Weight' => null, - 'Remark' => null, - 'UpdateTime' => null, - ]; - } - if(!isNullOrEmpty($SubDomain)){ - $list = array_values(array_filter($list, function($v) use ($SubDomain){ - return strcasecmp($v['Name'], $SubDomain) === 0; - })); - }else{ - if(!isNullOrEmpty($KeyWord)){ - $list = array_values(array_filter($list, function($v) use ($KeyWord){ - return strpos($v['Name'], $KeyWord) !== false || strpos($v['Value'], $KeyWord) !== false; - })); - } - if(!isNullOrEmpty($Value)){ - $list = array_values(array_filter($list, function($v) use ($Value){ - return $v['Value'] == $Value; - })); - } - if(!isNullOrEmpty($Type)){ - $list = array_values(array_filter($list, function($v) use ($Type){ - return $v['Type'] == $Type; - })); - } - } - return ['total' => count($data['resource_record']), 'list' => $list]; - } - return false; - } - - //获取子域名解析记录列表 - public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null) - { - return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line); - } - - //获取解析记录详细信息 - public function getDomainRecordInfo($RecordId) - { - return false; - } - - //添加解析记录 - public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - if ($Name == '@') $Name = ''; - $param = ['domain' => $this->domain, 'rrtype' => $Type, 'rrhost' => $Name, 'rrvalue' => $Value, 'rrttl' => $TTL]; - if ($Type == 'MX') $param['rrdistance'] = intval($MX); - $data = $this->send_reuqest('dnsAddRecord', $param); - return is_array($data) ? $data['record_id'] : false; - } - - //修改解析记录 - public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - if ($Name == '@') $Name = ''; - $param = ['domain' => $this->domain, 'rrid' => $RecordId, 'rrtype' => $Type, 'rrhost' => $Name, 'rrvalue' => $Value, 'rrttl' => $TTL]; - if ($Type == 'MX') $param['rrdistance'] = intval($MX); - $data = $this->send_reuqest('dnsUpdateRecord', $param); - return is_array($data); - } - - //修改解析记录备注 - public function updateDomainRecordRemark($RecordId, $Remark) - { - return false; - } - - //删除解析记录 - public function deleteDomainRecord($RecordId) - { - $param = ['domain' => $this->domain, 'rrid' => $RecordId]; - $data = $this->send_reuqest('dnsDeleteRecord', $param); - return is_array($data); - } - - //设置解析记录状态 - public function setDomainRecordStatus($RecordId, $Status) - { - return false; - } - - //获取解析记录操作日志 - public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) - { - return false; - } - - //获取解析线路列表 - public function getRecordLine() - { - return ['default' => ['name' => '默认', 'parent' => null]]; - } - - //获取域名信息 - public function getDomainInfo() - { - return false; - } - - //获取域名最低TTL - public function getMinTTL() - { - return false; - } - - public function addDomain($Domain) - { - return false; - } - - private function send_reuqest($operation, $param = null) - { - $url = $this->baseUrl . $operation; - - $params = [ - 'version' => $this->version, - 'type' => 'json', - 'key' => $this->apikey, - ]; - if($param){ - $params = array_merge($params, $param); - } - - $url .= '?' . http_build_query($params); - - try{ - $response = http_request($url, null, null, null, null, $this->proxy); - }catch(Exception $e){ - $this->setError($e->getMessage()); - return false; - } - - $arr = json_decode($response['body'], true); - if (isset($arr['reply']['code'])) { - if ($arr['reply']['code'] == 300) { - return $arr['reply']; - } else { - $this->setError(isset($arr['reply']['detail']) ? $arr['reply']['detail'] : '未知错误'); - return false; - } - } else { - $this->setError($response['body']); - return false; - } - } - - private function setError($message) - { - $this->error = $message; - //file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND); - } -} +apikey = $config['sk']; + $this->domain = $config['domain']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + } + + public function getError() + { + return $this->error; + } + + public function check() + { + if ($this->getDomainList() !== false) { + return true; + } + return false; + } + + //获取域名列表 + public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20) + { + $param = ['page' => $PageNumber, 'pageSize' => $PageSize]; + $data = $this->send_reuqest('listDomains', $param); + if ($data) { + $list = []; + if($data['domains']){ + foreach ($data['domains'] as $row) { + $list[] = [ + 'DomainId' => $row['domain'], + 'Domain' => $row['domain'], + 'RecordCount' => 0, + ]; + } + } + return ['total' => $data['pager']['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' => $this->domain]; + $data = $this->send_reuqest('dnsListRecords', $param); + 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, + 'Type' => $row['type'], + 'Value' => $row['value'], + 'Line' => 'default', + 'TTL' => $row['ttl'], + 'MX' => isset($row['distance']) ? $row['distance'] : null, + 'Status' => '1', + 'Weight' => null, + 'Remark' => null, + 'UpdateTime' => null, + ]; + } + if(!isNullOrEmpty($SubDomain)){ + $list = array_values(array_filter($list, function($v) use ($SubDomain){ + return strcasecmp($v['Name'], $SubDomain) === 0; + })); + }else{ + if(!isNullOrEmpty($KeyWord)){ + $list = array_values(array_filter($list, function($v) use ($KeyWord){ + return strpos($v['Name'], $KeyWord) !== false || strpos($v['Value'], $KeyWord) !== false; + })); + } + if(!isNullOrEmpty($Value)){ + $list = array_values(array_filter($list, function($v) use ($Value){ + return $v['Value'] == $Value; + })); + } + if(!isNullOrEmpty($Type)){ + $list = array_values(array_filter($list, function($v) use ($Type){ + return $v['Type'] == $Type; + })); + } + } + return ['total' => count($data['resource_record']), 'list' => $list]; + } + return false; + } + + //获取子域名解析记录列表 + public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null) + { + return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line); + } + + //获取解析记录详细信息 + public function getDomainRecordInfo($RecordId) + { + return false; + } + + //添加解析记录 + public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + if ($Name == '@') $Name = ''; + $param = ['domain' => $this->domain, 'rrtype' => $Type, 'rrhost' => $Name, 'rrvalue' => $Value, 'rrttl' => $TTL]; + if ($Type == 'MX') $param['rrdistance'] = intval($MX); + $data = $this->send_reuqest('dnsAddRecord', $param); + return is_array($data) ? $data['record_id'] : false; + } + + //修改解析记录 + public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + if ($Name == '@') $Name = ''; + $param = ['domain' => $this->domain, 'rrid' => $RecordId, 'rrtype' => $Type, 'rrhost' => $Name, 'rrvalue' => $Value, 'rrttl' => $TTL]; + if ($Type == 'MX') $param['rrdistance'] = intval($MX); + $data = $this->send_reuqest('dnsUpdateRecord', $param); + return is_array($data); + } + + //修改解析记录备注 + public function updateDomainRecordRemark($RecordId, $Remark) + { + return false; + } + + //删除解析记录 + public function deleteDomainRecord($RecordId) + { + $param = ['domain' => $this->domain, 'rrid' => $RecordId]; + $data = $this->send_reuqest('dnsDeleteRecord', $param); + return is_array($data); + } + + //设置解析记录状态 + public function setDomainRecordStatus($RecordId, $Status) + { + return false; + } + + //获取解析记录操作日志 + public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) + { + return false; + } + + //获取解析线路列表 + public function getRecordLine() + { + return ['default' => ['name' => '默认', 'parent' => null]]; + } + + //获取域名信息 + public function getDomainInfo() + { + return false; + } + + //获取域名最低TTL + public function getMinTTL() + { + return false; + } + + public function addDomain($Domain) + { + return false; + } + + private function send_reuqest($operation, $param = null) + { + $url = $this->baseUrl . $operation; + + $params = [ + 'version' => $this->version, + 'type' => 'json', + 'key' => $this->apikey, + ]; + if($param){ + $params = array_merge($params, $param); + } + + $url .= '?' . http_build_query($params); + + try{ + $response = http_request($url, null, null, null, null, $this->proxy); + }catch(Exception $e){ + $this->setError($e->getMessage()); + return false; + } + + $arr = json_decode($response['body'], true); + if (isset($arr['reply']['code'])) { + if ($arr['reply']['code'] == 300) { + return $arr['reply']; + } else { + $this->setError(isset($arr['reply']['detail']) ? $arr['reply']['detail'] : '未知错误'); + return false; + } + } else { + $this->setError($response['body']); + return false; + } + } + + private function setError($message) + { + $this->error = $message; + //file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND); + } +} diff --git a/app/lib/dns/powerdns.php b/app/lib/dns/powerdns.php index 4ac0fdd..339131e 100644 --- a/app/lib/dns/powerdns.php +++ b/app/lib/dns/powerdns.php @@ -1,425 +1,425 @@ -url = 'http://' . $config['ak'] . ':' . $config['sk'] . '/api/v1'; - $this->apikey = $config['ext']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - $this->domain = $config['domain']; - $this->domainid = $config['domainid']; - } - - public function getError() - { - return $this->error; - } - - public function check() - { - if ($this->getDomainList() !== false) { - return true; - } - return false; - } - - //获取域名列表 - public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20) - { - $data = $this->send_reuqest('GET', '/servers/' . $this->server_id . '/zones'); - if ($data) { - $list = []; - foreach ($data as $row) { - $list[] = [ - 'DomainId' => $row['id'], - 'Domain' => rtrim($row['name'], '.'), - 'RecordCount' => 0, - ]; - } - return ['total' => count($list), 'list' => $list]; - } - return false; - } - - //获取解析记录列表 - public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null) - { - $data = $this->send_reuqest('GET', '/servers/' . $this->server_id . '/zones/' . $this->domainid); - if ($data) { - $list = []; - $rrset_id = 0; - foreach ($data['rrsets'] as &$row) { - $rrset_id++; - $name = $row['name'] == $this->domainid ? '@' : str_replace('.' . $this->domainid, '', $row['name']); - $row['host'] = $name; - $row['id'] = $rrset_id; - $record_id = 0; - foreach ($row['records'] as &$record) { - $record_id++; - $record['id'] = $record_id; - $remark = !empty($row['comments']) ? $row['comments'][0]['content'] : null; - $value = $record['content']; - if ($row['type'] == 'MX') list($record['mx'], $value) = explode(' ', $record['content']); - $list[] = [ - 'RecordId' => $rrset_id . '_' . $record_id, - 'Domain' => $this->domain, - 'Name' => $name, - 'Type' => $row['type'], - 'Value' => $value, - 'Line' => 'default', - 'TTL' => $row['ttl'], - 'MX' => isset($record['mx']) ? $record['mx'] : null, - 'Status' => $record['disabled'] ? '0' : '1', - 'Weight' => null, - 'Remark' => $remark, - 'UpdateTime' => null, - ]; - } - } - cache('powerdns_' . $this->domainid, $data['rrsets'], 86400); - if (!isNullOrEmpty($SubDomain)) { - $list = array_values(array_filter($list, function ($v) use ($SubDomain) { - return strcasecmp($v['Name'], $SubDomain) === 0; - })); - } else { - if (!isNullOrEmpty($KeyWord)) { - $list = array_values(array_filter($list, function ($v) use ($KeyWord) { - return strpos($v['Name'], $KeyWord) !== false || strpos($v['Value'], $KeyWord) !== false; - })); - } - if (!isNullOrEmpty($Value)) { - $list = array_values(array_filter($list, function ($v) use ($Value) { - return $v['Value'] == $Value; - })); - } - if (!isNullOrEmpty($Type)) { - $list = array_values(array_filter($list, function ($v) use ($Type) { - return $v['Type'] == $Type; - })); - } - if (!isNullOrEmpty($Status)) { - $list = array_values(array_filter($list, function ($v) use ($Status) { - return $v['Status'] == $Status; - })); - } - } - return ['total' => count($list), 'list' => $list]; - } - return false; - } - - //获取子域名解析记录列表 - public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null) - { - return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line); - } - - //获取解析记录详细信息 - public function getDomainRecordInfo($RecordId) - { - return false; - } - - //添加解析记录 - public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - if ($Type == 'TXT' && substr($Value, 0, 1) != '"') $Value = '"' . $Value . '"'; - if (($Type == 'CNAME' || $Type == 'MX') && substr($Value, -1) != '.') $Value .= '.'; - if ($Type == 'MX') $Value = intval($MX) . ' ' . $Value; - $records = []; - $rrsets = cache('powerdns_' . $this->domainid); - if ($rrsets) { - $rrsets_filter = array_filter($rrsets, function ($row) use ($Name, $Type) { - return $row['host'] == $Name && $row['type'] == $Type; - }); - if (!empty($rrsets_filter)) { - $rrset = $rrsets_filter[array_key_first($rrsets_filter)]; - $records = $rrset['records']; - $records_filter = array_filter($records, function ($record) use ($Value) { - return $record['content'] == $Value; - }); - if (!empty($records_filter)) { - $this->setError('已存在相同记录'); - return false; - } - } - } - $records[] = ['content' => $Value, 'disabled' => false]; - return $this->rrset_replace($Name, $Type, $TTL, $records, $Remark); - } - - //修改解析记录 - public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - if ($Type == 'TXT' && substr($Value, 0, 1) != '"') $Value = '"' . $Value . '"'; - if (($Type == 'CNAME' || $Type == 'MX') && substr($Value, -1) != '.') $Value .= '.'; - if ($Type == 'MX') $Value = intval($MX) . ' ' . $Value; - $rrsets = cache('powerdns_' . $this->domainid); - $add = false; - $res = false; - if ($rrsets) { - [$rrset_id, $record_id] = explode('_', $RecordId); - $exist = false; - foreach ($rrsets as &$rrset) { - if ($rrset['id'] == $rrset_id) { - $records = $rrset['records']; - $records_filter = array_filter($records, function ($record) use ($Value, $record_id) { - return $record['content'] == $Value && $record['id'] != $record_id; - }); - if (!empty($records_filter)) { - $this->setError('已存在相同记录'); - return false; - } - foreach ($records as $i => &$record) { - if ($record['id'] == $record_id) { - $exist = true; - if ($rrset['host'] == $Name && $rrset['type'] == $Type) { - $record['content'] = $Value; - } else { - unset($records[$i]); - $add = true; - } - break; - } - } - if (!$exist) break; - $records = array_values($records); - if (!empty($records)) { - $res = $this->rrset_replace($rrset['host'], $rrset['type'], $TTL, $records, $Remark); - } else { - $res = $this->rrset_delete($rrset['host'], $rrset['type']); - } - $rrset['records'] = $records; - break; - } - } - if (!$exist) { - $this->setError('记录不存在,请刷新页面重试'); - return false; - } - cache('powerdns_' . $this->domainid, $rrsets, 86400); - if ($res && $add) { - $res = $this->addDomainRecord($Name, $Type, $Value, $Line, $TTL, $MX, $Weight, $Remark); - } - return $res; - } else { - $records[] = ['content' => $Value, 'disabled' => false]; - return $this->addDomainRecord($Name, $Type, $Value, $Line, $TTL, $MX, $Weight, $Remark); - } - } - - //修改解析记录备注 - public function updateDomainRecordRemark($RecordId, $Remark) - { - return false; - } - - //删除解析记录 - public function deleteDomainRecord($RecordId) - { - $rrsets = cache('powerdns_' . $this->domainid); - if (!$rrsets) { - $this->setError('记录不存在,请刷新页面重试'); - return false; - } - [$rrset_id, $record_id] = explode('_', $RecordId); - $exist = false; - $res = false; - foreach ($rrsets as &$rrset) { - if ($rrset['id'] == $rrset_id) { - $records = $rrset['records']; - foreach ($records as $i => &$record) { - if ($record['id'] == $record_id) { - $exist = true; - unset($records[$i]); - break; - } - } - if (!$exist) break; - $records = array_values($records); - if (!empty($records)) { - $res = $this->rrset_replace($rrset['host'], $rrset['type'], $rrset['ttl'], $records); - } else { - $res = $this->rrset_delete($rrset['host'], $rrset['type']); - } - $rrset['records'] = $records; - break; - } - } - if (!$exist) { - $this->setError('记录不存在,请刷新页面重试'); - return false; - } - cache('powerdns_' . $this->domainid, $rrsets, 86400); - return $res; - } - - //设置解析记录状态 - public function setDomainRecordStatus($RecordId, $Status) - { - $rrsets = cache('powerdns_' . $this->domainid); - if (!$rrsets) { - $this->setError('记录不存在,请刷新页面重试'); - return false; - } - [$rrset_id, $record_id] = explode('_', $RecordId); - $exist = false; - $res = false; - foreach ($rrsets as &$rrset) { - if ($rrset['id'] == $rrset_id) { - $records = $rrset['records']; - foreach ($records as &$record) { - if ($record['id'] == $record_id) { - $exist = true; - $record['disabled'] = $Status == '0'; - break; - } - } - if (!$exist) break; - $res = $this->rrset_replace($rrset['host'], $rrset['type'], $rrset['ttl'], $records); - $rrset['records'] = $records; - break; - } - } - if (!$exist) { - $this->setError('记录不存在,请刷新页面重试'); - return false; - } - cache('powerdns_' . $this->domainid, $rrsets, 86400); - return $res; - } - - //获取解析记录操作日志 - 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 false; - } - - public function addDomain($Domain) - { - if (substr($Domain, -1) != '.') { - $Domain .= '.'; - } - $param = [ - 'name' => $Domain, - 'kind' => 'Native', - 'soa_edit_api' => 'INCREASE', - ]; - $result = $this->send_reuqest('POST', '/servers/' . $this->server_id . '/zones', $param); - if ($result) { - return ['id' => $result['id'], 'name' => rtrim($result['name'], '.')]; - } - return false; - } - - private function rrset_replace($host, $type, $ttl, $records, $remark = null) - { - $name = $host == '@' ? $this->domainid : $host . '.' . $this->domainid; - $rrset = [ - 'name' => $name, - 'type' => $type, - 'ttl' => intval($ttl), - 'changetype' => 'REPLACE', - 'records' => $records, - 'comments' => [], - ]; - if (!empty($remark)) { - $rrset['comments'] = [ - ['account' => '', 'content' => $remark] - ]; - } - $param = [ - 'rrsets' => [ - $rrset - ], - ]; - return $this->send_reuqest('PATCH', '/servers/' . $this->server_id . '/zones/' . $this->domainid, $param); - } - - private function rrset_delete($host, $type) - { - $name = $host == '@' ? $this->domainid : $host . '.' . $this->domainid; - $param = [ - 'rrsets' => [ - [ - 'name' => $name, - 'type' => $type, - 'changetype' => 'DELETE', - ] - ], - ]; - return $this->send_reuqest('PATCH', '/servers/' . $this->server_id . '/zones/' . $this->domainid, $param); - } - - private function send_reuqest($method, $path, $params = null) - { - $url = $this->url . $path; - $headers['X-API-Key'] = $this->apikey; - $body = null; - if ($method == 'GET' || $method == 'DELETE') { - 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'] < 400) { - return is_array($arr) ? $arr : true; - } elseif (isset($arr['error'])) { - $this->setError($arr['error']); - return false; - } elseif (isset($arr['errors'])) { - $this->setError(implode(',', $arr['errors'])); - return false; - } else { - $this->setError($response['body']); - return false; - } - } - - private function setError($message) - { - $this->error = $message; - } -} +url = 'http://' . $config['ak'] . ':' . $config['sk'] . '/api/v1'; + $this->apikey = $config['ext']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + $this->domain = $config['domain']; + $this->domainid = $config['domainid']; + } + + public function getError() + { + return $this->error; + } + + public function check() + { + if ($this->getDomainList() !== false) { + return true; + } + return false; + } + + //获取域名列表 + public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20) + { + $data = $this->send_reuqest('GET', '/servers/' . $this->server_id . '/zones'); + if ($data) { + $list = []; + foreach ($data as $row) { + $list[] = [ + 'DomainId' => $row['id'], + 'Domain' => rtrim($row['name'], '.'), + 'RecordCount' => 0, + ]; + } + return ['total' => count($list), 'list' => $list]; + } + return false; + } + + //获取解析记录列表 + public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null) + { + $data = $this->send_reuqest('GET', '/servers/' . $this->server_id . '/zones/' . $this->domainid); + if ($data) { + $list = []; + $rrset_id = 0; + foreach ($data['rrsets'] as &$row) { + $rrset_id++; + $name = $row['name'] == $this->domainid ? '@' : str_replace('.' . $this->domainid, '', $row['name']); + $row['host'] = $name; + $row['id'] = $rrset_id; + $record_id = 0; + foreach ($row['records'] as &$record) { + $record_id++; + $record['id'] = $record_id; + $remark = !empty($row['comments']) ? $row['comments'][0]['content'] : null; + $value = $record['content']; + if ($row['type'] == 'MX') list($record['mx'], $value) = explode(' ', $record['content']); + $list[] = [ + 'RecordId' => $rrset_id . '_' . $record_id, + 'Domain' => $this->domain, + 'Name' => $name, + 'Type' => $row['type'], + 'Value' => $value, + 'Line' => 'default', + 'TTL' => $row['ttl'], + 'MX' => isset($record['mx']) ? $record['mx'] : null, + 'Status' => $record['disabled'] ? '0' : '1', + 'Weight' => null, + 'Remark' => $remark, + 'UpdateTime' => null, + ]; + } + } + cache('powerdns_' . $this->domainid, $data['rrsets'], 86400); + if (!isNullOrEmpty($SubDomain)) { + $list = array_values(array_filter($list, function ($v) use ($SubDomain) { + return strcasecmp($v['Name'], $SubDomain) === 0; + })); + } else { + if (!isNullOrEmpty($KeyWord)) { + $list = array_values(array_filter($list, function ($v) use ($KeyWord) { + return strpos($v['Name'], $KeyWord) !== false || strpos($v['Value'], $KeyWord) !== false; + })); + } + if (!isNullOrEmpty($Value)) { + $list = array_values(array_filter($list, function ($v) use ($Value) { + return $v['Value'] == $Value; + })); + } + if (!isNullOrEmpty($Type)) { + $list = array_values(array_filter($list, function ($v) use ($Type) { + return $v['Type'] == $Type; + })); + } + if (!isNullOrEmpty($Status)) { + $list = array_values(array_filter($list, function ($v) use ($Status) { + return $v['Status'] == $Status; + })); + } + } + return ['total' => count($list), 'list' => $list]; + } + return false; + } + + //获取子域名解析记录列表 + public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null) + { + return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line); + } + + //获取解析记录详细信息 + public function getDomainRecordInfo($RecordId) + { + return false; + } + + //添加解析记录 + public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + if ($Type == 'TXT' && substr($Value, 0, 1) != '"') $Value = '"' . $Value . '"'; + if (($Type == 'CNAME' || $Type == 'MX') && substr($Value, -1) != '.') $Value .= '.'; + if ($Type == 'MX') $Value = intval($MX) . ' ' . $Value; + $records = []; + $rrsets = cache('powerdns_' . $this->domainid); + if ($rrsets) { + $rrsets_filter = array_filter($rrsets, function ($row) use ($Name, $Type) { + return $row['host'] == $Name && $row['type'] == $Type; + }); + if (!empty($rrsets_filter)) { + $rrset = $rrsets_filter[array_key_first($rrsets_filter)]; + $records = $rrset['records']; + $records_filter = array_filter($records, function ($record) use ($Value) { + return $record['content'] == $Value; + }); + if (!empty($records_filter)) { + $this->setError('已存在相同记录'); + return false; + } + } + } + $records[] = ['content' => $Value, 'disabled' => false]; + return $this->rrset_replace($Name, $Type, $TTL, $records, $Remark); + } + + //修改解析记录 + public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + if ($Type == 'TXT' && substr($Value, 0, 1) != '"') $Value = '"' . $Value . '"'; + if (($Type == 'CNAME' || $Type == 'MX') && substr($Value, -1) != '.') $Value .= '.'; + if ($Type == 'MX') $Value = intval($MX) . ' ' . $Value; + $rrsets = cache('powerdns_' . $this->domainid); + $add = false; + $res = false; + if ($rrsets) { + [$rrset_id, $record_id] = explode('_', $RecordId); + $exist = false; + foreach ($rrsets as &$rrset) { + if ($rrset['id'] == $rrset_id) { + $records = $rrset['records']; + $records_filter = array_filter($records, function ($record) use ($Value, $record_id) { + return $record['content'] == $Value && $record['id'] != $record_id; + }); + if (!empty($records_filter)) { + $this->setError('已存在相同记录'); + return false; + } + foreach ($records as $i => &$record) { + if ($record['id'] == $record_id) { + $exist = true; + if ($rrset['host'] == $Name && $rrset['type'] == $Type) { + $record['content'] = $Value; + } else { + unset($records[$i]); + $add = true; + } + break; + } + } + if (!$exist) break; + $records = array_values($records); + if (!empty($records)) { + $res = $this->rrset_replace($rrset['host'], $rrset['type'], $TTL, $records, $Remark); + } else { + $res = $this->rrset_delete($rrset['host'], $rrset['type']); + } + $rrset['records'] = $records; + break; + } + } + if (!$exist) { + $this->setError('记录不存在,请刷新页面重试'); + return false; + } + cache('powerdns_' . $this->domainid, $rrsets, 86400); + if ($res && $add) { + $res = $this->addDomainRecord($Name, $Type, $Value, $Line, $TTL, $MX, $Weight, $Remark); + } + return $res; + } else { + $records[] = ['content' => $Value, 'disabled' => false]; + return $this->addDomainRecord($Name, $Type, $Value, $Line, $TTL, $MX, $Weight, $Remark); + } + } + + //修改解析记录备注 + public function updateDomainRecordRemark($RecordId, $Remark) + { + return false; + } + + //删除解析记录 + public function deleteDomainRecord($RecordId) + { + $rrsets = cache('powerdns_' . $this->domainid); + if (!$rrsets) { + $this->setError('记录不存在,请刷新页面重试'); + return false; + } + [$rrset_id, $record_id] = explode('_', $RecordId); + $exist = false; + $res = false; + foreach ($rrsets as &$rrset) { + if ($rrset['id'] == $rrset_id) { + $records = $rrset['records']; + foreach ($records as $i => &$record) { + if ($record['id'] == $record_id) { + $exist = true; + unset($records[$i]); + break; + } + } + if (!$exist) break; + $records = array_values($records); + if (!empty($records)) { + $res = $this->rrset_replace($rrset['host'], $rrset['type'], $rrset['ttl'], $records); + } else { + $res = $this->rrset_delete($rrset['host'], $rrset['type']); + } + $rrset['records'] = $records; + break; + } + } + if (!$exist) { + $this->setError('记录不存在,请刷新页面重试'); + return false; + } + cache('powerdns_' . $this->domainid, $rrsets, 86400); + return $res; + } + + //设置解析记录状态 + public function setDomainRecordStatus($RecordId, $Status) + { + $rrsets = cache('powerdns_' . $this->domainid); + if (!$rrsets) { + $this->setError('记录不存在,请刷新页面重试'); + return false; + } + [$rrset_id, $record_id] = explode('_', $RecordId); + $exist = false; + $res = false; + foreach ($rrsets as &$rrset) { + if ($rrset['id'] == $rrset_id) { + $records = $rrset['records']; + foreach ($records as &$record) { + if ($record['id'] == $record_id) { + $exist = true; + $record['disabled'] = $Status == '0'; + break; + } + } + if (!$exist) break; + $res = $this->rrset_replace($rrset['host'], $rrset['type'], $rrset['ttl'], $records); + $rrset['records'] = $records; + break; + } + } + if (!$exist) { + $this->setError('记录不存在,请刷新页面重试'); + return false; + } + cache('powerdns_' . $this->domainid, $rrsets, 86400); + return $res; + } + + //获取解析记录操作日志 + 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 false; + } + + public function addDomain($Domain) + { + if (substr($Domain, -1) != '.') { + $Domain .= '.'; + } + $param = [ + 'name' => $Domain, + 'kind' => 'Native', + 'soa_edit_api' => 'INCREASE', + ]; + $result = $this->send_reuqest('POST', '/servers/' . $this->server_id . '/zones', $param); + if ($result) { + return ['id' => $result['id'], 'name' => rtrim($result['name'], '.')]; + } + return false; + } + + private function rrset_replace($host, $type, $ttl, $records, $remark = null) + { + $name = $host == '@' ? $this->domainid : $host . '.' . $this->domainid; + $rrset = [ + 'name' => $name, + 'type' => $type, + 'ttl' => intval($ttl), + 'changetype' => 'REPLACE', + 'records' => $records, + 'comments' => [], + ]; + if (!empty($remark)) { + $rrset['comments'] = [ + ['account' => '', 'content' => $remark] + ]; + } + $param = [ + 'rrsets' => [ + $rrset + ], + ]; + return $this->send_reuqest('PATCH', '/servers/' . $this->server_id . '/zones/' . $this->domainid, $param); + } + + private function rrset_delete($host, $type) + { + $name = $host == '@' ? $this->domainid : $host . '.' . $this->domainid; + $param = [ + 'rrsets' => [ + [ + 'name' => $name, + 'type' => $type, + 'changetype' => 'DELETE', + ] + ], + ]; + return $this->send_reuqest('PATCH', '/servers/' . $this->server_id . '/zones/' . $this->domainid, $param); + } + + private function send_reuqest($method, $path, $params = null) + { + $url = $this->url . $path; + $headers['X-API-Key'] = $this->apikey; + $body = null; + if ($method == 'GET' || $method == 'DELETE') { + 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'] < 400) { + return is_array($arr) ? $arr : true; + } elseif (isset($arr['error'])) { + $this->setError($arr['error']); + return false; + } elseif (isset($arr['errors'])) { + $this->setError(implode(',', $arr['errors'])); + return false; + } else { + $this->setError($response['body']); + return false; + } + } + + private function setError($message) + { + $this->error = $message; + } +} diff --git a/app/lib/dns/west.php b/app/lib/dns/west.php index 9d51b8a..adfbf9d 100644 --- a/app/lib/dns/west.php +++ b/app/lib/dns/west.php @@ -1,215 +1,215 @@ -username = $config['ak']; - $this->api_password = $config['sk']; - $this->domain = $config['domain']; - $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; - } - - public function getError() - { - return $this->error; - } - - public function check() - { - if ($this->getDomainList() != false) { - return true; - } - return false; - } - - //获取域名列表 - public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20) - { - $param = ['act' => 'getdomains', 'page' => $PageNumber, 'limit' => $PageSize, 'domain' => $KeyWord]; - $data = $this->execute('/domain/', $param); - if ($data) { - $list = []; - foreach ($data['items'] as $row) { - $list[] = [ - 'DomainId' => $row['domain'], - 'Domain' => $row['domain'], - 'RecordCount' => 0, - ]; - } - return ['total' => $data['total'], 'list' => $list]; - } - return false; - } - - //获取解析记录列表 - public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null) - { - $param = ['act' => 'getdnsrecord', 'domain' => $this->domain, 'type' => $Type, 'line' => $Line, 'host' => $KeyWord, 'value' => $Value, 'pageno' => $PageNumber, 'limit' => $PageSize]; - if (!isNullOrEmpty(($SubDomain))) { - $param['host'] = $SubDomain; - } - $data = $this->execute('/domain/', $param); - if ($data) { - $list = []; - foreach ($data['items'] as $row) { - $list[] = [ - 'RecordId' => $row['id'], - 'Domain' => $this->domain, - 'Name' => $row['item'], - 'Type' => $row['type'], - 'Value' => $row['value'], - 'Line' => $row['line'], - 'TTL' => $row['ttl'], - 'MX' => $row['level'], - 'Status' => $row['pause'] == 1 ? '0' : '1', - 'Weight' => null, - 'Remark' => null, - 'UpdateTime' => null, - ]; - } - return ['total' => $data['total'], 'list' => $list]; - } - return false; - } - - //获取子域名解析记录列表 - public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null) - { - if ($SubDomain == '') $SubDomain = '@'; - return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line); - } - - //获取解析记录详细信息 - public function getDomainRecordInfo($RecordId) - { - return false; - } - - //添加解析记录 - public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $param = ['act' => 'adddnsrecord', 'domain' => $this->domain, 'host' => $Name, 'type' => $this->convertType($Type), 'value' => $Value, 'level' => $MX, 'ttl' => intval($TTL), 'line' => $Line]; - $data = $this->execute('/domain/', $param); - return is_array($data) ? $data['id'] : false; - } - - //修改解析记录 - public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) - { - $param = ['act' => 'moddnsrecord', 'domain' => $this->domain, 'id' => $RecordId, 'type' => $this->convertType($Type), 'value' => $Value, 'level' => $MX, 'ttl' => intval($TTL), 'line' => $Line]; - $data = $this->execute('/domain/', $param); - return is_array($data); - } - - //修改解析记录备注 - public function updateDomainRecordRemark($RecordId, $Remark) - { - return false; - } - - //删除解析记录 - public function deleteDomainRecord($RecordId) - { - $param = ['act' => 'deldnsrecord', 'domain' => $this->domain, 'id' => $RecordId]; - $data = $this->execute('/domain/', $param); - return is_array($data); - } - - //设置解析记录状态 - public function setDomainRecordStatus($RecordId, $Status) - { - $param = ['act' => 'pause', 'domain' => $this->domain, 'id' => $RecordId, 'val' => $Status == '1' ? '0' : '1']; - $data = $this->execute('/domain/', $param); - return $data !== false; - } - - //获取解析记录操作日志 - public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) - { - return false; - } - - //获取解析线路列表 - public function getRecordLine() - { - return [ - '' => ['name' => '默认', 'parent' => null], - 'LTEL' => ['name' => '电信', 'parent' => null], - 'LCNC' => ['name' => '联通', 'parent' => null], - 'LMOB' => ['name' => '移动', 'parent' => null], - 'LEDU' => ['name' => '教育网', 'parent' => null], - 'LSEO' => ['name' => '搜索引擎', 'parent' => null], - 'LFOR' => ['name' => '境外', 'parent' => null], - ]; - } - - //获取域名信息 - public function getDomainInfo() - { - return false; - } - - //获取域名最低TTL - public function getMinTTL() - { - return false; - } - - public function addDomain($Domain) - { - return false; - } - - private function convertType($type) - { - return $type; - } - - private function execute($path, $params) - { - $params['username'] = $this->username; - $params['time'] = getMillisecond(); - $params['token'] = md5($this->username.$this->api_password.$params['time']); - try{ - $response = http_request($this->baseUrl . $path, http_build_query($params), null, null, null, $this->proxy); - }catch(\Exception $e){ - $this->setError($e->getMessage()); - return false; - } - $response = mb_convert_encoding($response['body'], 'UTF-8', 'GBK'); - $arr = json_decode($response, true); - if ($arr) { - if ($arr['result'] == 200) { - return isset($arr['data']) ? $arr['data'] : []; - } else { - $this->setError($arr['msg']); - return false; - } - } else { - $this->setError('返回数据解析失败'); - return false; - } - } - - private function setError($message) - { - $this->error = $message; - //file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND); - } -} +username = $config['ak']; + $this->api_password = $config['sk']; + $this->domain = $config['domain']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + } + + public function getError() + { + return $this->error; + } + + public function check() + { + if ($this->getDomainList() != false) { + return true; + } + return false; + } + + //获取域名列表 + public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20) + { + $param = ['act' => 'getdomains', 'page' => $PageNumber, 'limit' => $PageSize, 'domain' => $KeyWord]; + $data = $this->execute('/domain/', $param); + if ($data) { + $list = []; + foreach ($data['items'] as $row) { + $list[] = [ + 'DomainId' => $row['domain'], + 'Domain' => $row['domain'], + 'RecordCount' => 0, + ]; + } + return ['total' => $data['total'], 'list' => $list]; + } + return false; + } + + //获取解析记录列表 + public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null) + { + $param = ['act' => 'getdnsrecord', 'domain' => $this->domain, 'type' => $Type, 'line' => $Line, 'host' => $KeyWord, 'value' => $Value, 'pageno' => $PageNumber, 'limit' => $PageSize]; + if (!isNullOrEmpty(($SubDomain))) { + $param['host'] = $SubDomain; + } + $data = $this->execute('/domain/', $param); + if ($data) { + $list = []; + foreach ($data['items'] as $row) { + $list[] = [ + 'RecordId' => $row['id'], + 'Domain' => $this->domain, + 'Name' => $row['item'], + 'Type' => $row['type'], + 'Value' => $row['value'], + 'Line' => $row['line'], + 'TTL' => $row['ttl'], + 'MX' => $row['level'], + 'Status' => $row['pause'] == 1 ? '0' : '1', + 'Weight' => null, + 'Remark' => null, + 'UpdateTime' => null, + ]; + } + return ['total' => $data['total'], 'list' => $list]; + } + return false; + } + + //获取子域名解析记录列表 + public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null) + { + if ($SubDomain == '') $SubDomain = '@'; + return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line); + } + + //获取解析记录详细信息 + public function getDomainRecordInfo($RecordId) + { + return false; + } + + //添加解析记录 + public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $param = ['act' => 'adddnsrecord', 'domain' => $this->domain, 'host' => $Name, 'type' => $this->convertType($Type), 'value' => $Value, 'level' => $MX, 'ttl' => intval($TTL), 'line' => $Line]; + $data = $this->execute('/domain/', $param); + return is_array($data) ? $data['id'] : false; + } + + //修改解析记录 + public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null) + { + $param = ['act' => 'moddnsrecord', 'domain' => $this->domain, 'id' => $RecordId, 'type' => $this->convertType($Type), 'value' => $Value, 'level' => $MX, 'ttl' => intval($TTL), 'line' => $Line]; + $data = $this->execute('/domain/', $param); + return is_array($data); + } + + //修改解析记录备注 + public function updateDomainRecordRemark($RecordId, $Remark) + { + return false; + } + + //删除解析记录 + public function deleteDomainRecord($RecordId) + { + $param = ['act' => 'deldnsrecord', 'domain' => $this->domain, 'id' => $RecordId]; + $data = $this->execute('/domain/', $param); + return is_array($data); + } + + //设置解析记录状态 + public function setDomainRecordStatus($RecordId, $Status) + { + $param = ['act' => 'pause', 'domain' => $this->domain, 'id' => $RecordId, 'val' => $Status == '1' ? '0' : '1']; + $data = $this->execute('/domain/', $param); + return $data !== false; + } + + //获取解析记录操作日志 + public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null) + { + return false; + } + + //获取解析线路列表 + public function getRecordLine() + { + return [ + '' => ['name' => '默认', 'parent' => null], + 'LTEL' => ['name' => '电信', 'parent' => null], + 'LCNC' => ['name' => '联通', 'parent' => null], + 'LMOB' => ['name' => '移动', 'parent' => null], + 'LEDU' => ['name' => '教育网', 'parent' => null], + 'LSEO' => ['name' => '搜索引擎', 'parent' => null], + 'LFOR' => ['name' => '境外', 'parent' => null], + ]; + } + + //获取域名信息 + public function getDomainInfo() + { + return false; + } + + //获取域名最低TTL + public function getMinTTL() + { + return false; + } + + public function addDomain($Domain) + { + return false; + } + + private function convertType($type) + { + return $type; + } + + private function execute($path, $params) + { + $params['username'] = $this->username; + $params['time'] = getMillisecond(); + $params['token'] = md5($this->username.$this->api_password.$params['time']); + try{ + $response = http_request($this->baseUrl . $path, http_build_query($params), null, null, null, $this->proxy); + }catch(\Exception $e){ + $this->setError($e->getMessage()); + return false; + } + $response = mb_convert_encoding($response['body'], 'UTF-8', 'GBK'); + $arr = json_decode($response, true); + if ($arr) { + if ($arr['result'] == 200) { + return isset($arr['data']) ? $arr['data'] : []; + } else { + $this->setError($arr['msg']); + return false; + } + } else { + $this->setError('返回数据解析失败'); + return false; + } + } + + private function setError($message) + { + $this->error = $message; + //file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND); + } +} diff --git a/app/middleware/AuthApi.php b/app/middleware/AuthApi.php index cba9c72..6930b41 100644 --- a/app/middleware/AuthApi.php +++ b/app/middleware/AuthApi.php @@ -1,47 +1,47 @@ - -1, 'msg' => '认证参数不能为空'])->code(403); - } - if ($timestamp < time() - 300 || $timestamp > time() + 300) { - return json(['code' => -1, 'msg' => '时间戳不合法'])->code(403); - } - $user = Db::name('user')->where('id', $uid)->find(); - if (!$user) { - return json(['code' => -1, 'msg' => '用户不存在'])->code(403); - } - if ($user['status'] == 0) { - return json(['code' => -1, 'msg' => '该用户已被封禁'])->code(403); - } - if ($user['is_api'] == 0) { - return json(['code' => -1, 'msg' => '该用户未开启API权限'])->code(403); - } - if (md5($uid.$timestamp.$user['apikey']) !== $sign) { - return json(['code' => -1, 'msg' => '签名错误'])->code(403); - } - - $user['type'] = 'user'; - $user['permission'] = []; - if ($user['level'] == 1) { - $user['permission'] = Db::name('permission')->where('uid', $uid)->column('domain'); - } - - $request->islogin = true; - $request->isApi = true; - $request->user = $user; - return $next($request); - } -} + -1, 'msg' => '认证参数不能为空'])->code(403); + } + if ($timestamp < time() - 300 || $timestamp > time() + 300) { + return json(['code' => -1, 'msg' => '时间戳不合法'])->code(403); + } + $user = Db::name('user')->where('id', $uid)->find(); + if (!$user) { + return json(['code' => -1, 'msg' => '用户不存在'])->code(403); + } + if ($user['status'] == 0) { + return json(['code' => -1, 'msg' => '该用户已被封禁'])->code(403); + } + if ($user['is_api'] == 0) { + return json(['code' => -1, 'msg' => '该用户未开启API权限'])->code(403); + } + if (md5($uid.$timestamp.$user['apikey']) !== $sign) { + return json(['code' => -1, 'msg' => '签名错误'])->code(403); + } + + $user['type'] = 'user'; + $user['permission'] = []; + if ($user['level'] == 1) { + $user['permission'] = Db::name('permission')->where('uid', $uid)->column('domain'); + } + + $request->islogin = true; + $request->isApi = true; + $request->user = $user; + return $next($request); + } +} diff --git a/app/middleware/AuthUser.php b/app/middleware/AuthUser.php index 3000e51..c2a9d3f 100644 --- a/app/middleware/AuthUser.php +++ b/app/middleware/AuthUser.php @@ -1,53 +1,53 @@ -where('id', $uid)->find(); - if ($user && $user['status'] == 1) { - $session = md5($user['id'].$user['password']); - if ($session === $sid && $expiretime > time()) { - $islogin = true; - } - $user['type'] = 'user'; - $user['permission'] = []; - if ($user['level'] == 1) { - $user['permission'] = Db::name('permission')->where('uid', $uid)->column('domain'); - } - } - } elseif ($type == 'domain') { - $user = Db::name('domain')->where('id', $uid)->find(); - if ($user && $user['is_sso'] == 1) { - $session = md5($user['id'].$user['name']); - if ($session === $sid && $expiretime > time()) { - $islogin = true; - } - $user['username'] = $user['name']; - $user['regtime'] = $user['addtime']; - $user['type'] = 'domain'; - $user['level'] = 0; - $user['permission'] = [$user['name']]; - } - } - } - } - $request->islogin = $islogin; - $request->user = $user; - return $next($request); - } -} +where('id', $uid)->find(); + if ($user && $user['status'] == 1) { + $session = md5($user['id'].$user['password']); + if ($session === $sid && $expiretime > time()) { + $islogin = true; + } + $user['type'] = 'user'; + $user['permission'] = []; + if ($user['level'] == 1) { + $user['permission'] = Db::name('permission')->where('uid', $uid)->column('domain'); + } + } + } elseif ($type == 'domain') { + $user = Db::name('domain')->where('id', $uid)->find(); + if ($user && $user['is_sso'] == 1) { + $session = md5($user['id'].$user['name']); + if ($session === $sid && $expiretime > time()) { + $islogin = true; + } + $user['username'] = $user['name']; + $user['regtime'] = $user['addtime']; + $user['type'] = 'domain'; + $user['level'] = 0; + $user['permission'] = [$user['name']]; + } + } + } + } + $request->islogin = $islogin; + $request->user = $user; + return $next($request); + } +} diff --git a/app/middleware/CheckLogin.php b/app/middleware/CheckLogin.php index b3689c1..294fd3a 100644 --- a/app/middleware/CheckLogin.php +++ b/app/middleware/CheckLogin.php @@ -1,19 +1,19 @@ -islogin) { - if ($request->isAjax() || !$request->isGet()) { - return json(['code' => -1, 'msg' => '未登录'])->code(401); - } - return redirect((string)url('/login')); - } - return $next($request); - } -} +islogin) { + if ($request->isAjax() || !$request->isGet()) { + return json(['code' => -1, 'msg' => '未登录'])->code(401); + } + return redirect((string)url('/login')); + } + return $next($request); + } +} diff --git a/app/middleware/RefererCheck.php b/app/middleware/RefererCheck.php index 7994ebd..9973215 100644 --- a/app/middleware/RefererCheck.php +++ b/app/middleware/RefererCheck.php @@ -1,25 +1,25 @@ -islogin); - View::assign('user', $request->user); - View::assign('cdnpublic', 'https://s4.zstatic.net/ajax/libs/'); - View::assign('skin', getAdminSkin()); - return $next($request); - } -} +islogin); + View::assign('user', $request->user); + View::assign('skin', getAdminSkin()); + return $next($request); + } +} diff --git a/app/service/CertDeployService.php b/app/service/CertDeployService.php index 047b841..ead2254 100644 --- a/app/service/CertDeployService.php +++ b/app/service/CertDeployService.php @@ -1,148 +1,148 @@ -where('id', $tid)->find(); - if (!$task) throw new Exception('该自动部署任务不存在', 102); - $this->task = $task; - - $this->aid = $task['aid']; - $this->client = DeployHelper::getModel($this->aid); - if (!$this->client) throw new Exception('该自动部署任务类型不存在', 102); - - $this->info = $task['info'] ? json_decode($task['info'], true) : []; - } - - public function process($isManual = false) - { - if ($this->task['status'] >= 1) return; - if ($this->task['retry'] >= 6 && !$isManual) { - throw new Exception('已超出最大重试次数('.$this->task['error'].')', 103); - } - - $order = Db::name('cert_order')->where('id', $this->task['oid'])->find(); - if(!$order) throw new Exception('SSL证书订单不存在', 102); - if($order['status'] == 4) throw new Exception('SSL证书订单已吊销', 102); - if($order['status'] != 3) throw new Exception('SSL证书订单未完成签发', 102); - if(empty($order['fullchain']) || empty($order['privatekey'])) throw new Exception('SSL证书或私钥内容不存在', 102); - - $this->lockTaskData(); - try { - $this->deploy($order['fullchain'], $order['privatekey']); - } finally { - $this->unlockTaskData(); - } - } - - //部署证书 - public function deploy($fullchain, $privatekey) - { - $this->client->setLogger(function ($txt) { - $this->saveLog($txt); - }); - $this->saveLog(date('Y-m-d H:i:s')); - $config = json_decode($this->task['config'], true); - $config['domainList'] = Db::name('cert_domain')->where('oid', $this->task['oid'])->order('sort', 'asc')->column('domain'); - try { - $this->client->deploy($fullchain, $privatekey, $config, $this->info); - $this->saveResult(1); - $this->saveLog('[Success] 证书部署成功'); - } catch (Exception $e) { - $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)]); - } - } - } - - //重置任务 - public function reset() - { - Db::name('cert_deploy')->where('id', $this->task['id'])->data(['status' => 0, 'retry' => 0, 'retrytime' => null, 'issend' => 0, 'islock' => 0])->update(); - //$file_name = app()->getRuntimePath().'log/'.$this->task['processid'].'.log'; - //if (file_exists($file_name)) unlink($file_name); - $this->task['status'] = 0; - $this->task['retry'] = 0; - } - - private function saveResult($status, $error = null, $retrytime = null) - { - $this->task['status'] = $status; - if (!empty($error) && strlen($error) > 300) { - $error = mb_strcut($error, 0, 300); - } - $update = ['status' => $status, 'error' => $error, 'retrytime' => $retrytime]; - if ($status == 1){ - $update['retry'] = 0; - $update['lasttime'] = date('Y-m-d H:i:s'); - } - $res = Db::name('cert_deploy')->where('id', $this->task['id'])->data($update); - if ($status < 0 || $retrytime) { - $this->task['retry']++; - $res->inc('retry'); - } - $res->update(); - if ($error) { - $this->saveLog('[Error] ' . $error); - } - } - - private function lockTaskData() - { - Db::startTrans(); - try { - $isLock = Db::name('cert_deploy')->where('id', $this->task['id'])->lock(true)->value('islock'); - if ($isLock == 1 && time() - strtotime($this->task['locktime']) < 3600) { - throw new Exception('部署任务处理中,请稍后再试'); - } - $update = ['islock' => 1, 'locktime' => date('Y-m-d H:i:s')]; - if (empty($this->task['processid'])) $this->task['processid'] = $update['processid'] = getSid(); - Db::name('cert_deploy')->where('id', $this->task['id'])->update($update); - Db::commit(); - } catch (Exception $e) { - Db::rollback(); - throw $e; - } - } - - private function unlockTaskData() - { - Db::name('cert_deploy')->where('id', $this->task['id'])->update(['islock' => 0]); - } - - private function saveLog($txt) - { - if (empty($this->task['processid'])) return; - if (!is_dir(app()->getRuntimePath() . 'log')) mkdir(app()->getRuntimePath() . 'log'); - $file_name = app()->getRuntimePath().'log/'.$this->task['processid'].'.log'; - $file_exists = file_exists($file_name); - file_put_contents($file_name, $txt . PHP_EOL, FILE_APPEND); - if (!$file_exists) { - @chmod($file_name, 0777); - } - if(php_sapi_name() == 'cli'){ - echo $txt . PHP_EOL; - } - } +where('id', $tid)->find(); + if (!$task) throw new Exception('该自动部署任务不存在', 102); + $this->task = $task; + + $this->aid = $task['aid']; + $this->client = DeployHelper::getModel($this->aid); + if (!$this->client) throw new Exception('该自动部署任务类型不存在', 102); + + $this->info = $task['info'] ? json_decode($task['info'], true) : []; + } + + public function process($isManual = false) + { + if ($this->task['status'] >= 1) return; + if ($this->task['retry'] >= 6 && !$isManual) { + throw new Exception('已超出最大重试次数('.$this->task['error'].')', 103); + } + + $order = Db::name('cert_order')->where('id', $this->task['oid'])->find(); + if(!$order) throw new Exception('SSL证书订单不存在', 102); + if($order['status'] == 4) throw new Exception('SSL证书订单已吊销', 102); + if($order['status'] != 3) throw new Exception('SSL证书订单未完成签发', 102); + if(empty($order['fullchain']) || empty($order['privatekey'])) throw new Exception('SSL证书或私钥内容不存在', 102); + + $this->lockTaskData(); + try { + $this->deploy($order['fullchain'], $order['privatekey']); + } finally { + $this->unlockTaskData(); + } + } + + //部署证书 + public function deploy($fullchain, $privatekey) + { + $this->client->setLogger(function ($txt) { + $this->saveLog($txt); + }); + $this->saveLog(date('Y-m-d H:i:s')); + $config = json_decode($this->task['config'], true); + $config['domainList'] = Db::name('cert_domain')->where('oid', $this->task['oid'])->order('sort', 'asc')->column('domain'); + try { + $this->client->deploy($fullchain, $privatekey, $config, $this->info); + $this->saveResult(1); + $this->saveLog('[Success] 证书部署成功'); + } catch (Exception $e) { + $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)]); + } + } + } + + //重置任务 + public function reset() + { + Db::name('cert_deploy')->where('id', $this->task['id'])->data(['status' => 0, 'retry' => 0, 'retrytime' => null, 'issend' => 0, 'islock' => 0])->update(); + //$file_name = app()->getRuntimePath().'log/'.$this->task['processid'].'.log'; + //if (file_exists($file_name)) unlink($file_name); + $this->task['status'] = 0; + $this->task['retry'] = 0; + } + + private function saveResult($status, $error = null, $retrytime = null) + { + $this->task['status'] = $status; + if (!empty($error) && strlen($error) > 300) { + $error = mb_strcut($error, 0, 300); + } + $update = ['status' => $status, 'error' => $error, 'retrytime' => $retrytime]; + if ($status == 1){ + $update['retry'] = 0; + $update['lasttime'] = date('Y-m-d H:i:s'); + } + $res = Db::name('cert_deploy')->where('id', $this->task['id'])->data($update); + if ($status < 0 || $retrytime) { + $this->task['retry']++; + $res->inc('retry'); + } + $res->update(); + if ($error) { + $this->saveLog('[Error] ' . $error); + } + } + + private function lockTaskData() + { + Db::startTrans(); + try { + $isLock = Db::name('cert_deploy')->where('id', $this->task['id'])->lock(true)->value('islock'); + if ($isLock == 1 && time() - strtotime($this->task['locktime']) < 3600) { + throw new Exception('部署任务处理中,请稍后再试'); + } + $update = ['islock' => 1, 'locktime' => date('Y-m-d H:i:s')]; + if (empty($this->task['processid'])) $this->task['processid'] = $update['processid'] = getSid(); + Db::name('cert_deploy')->where('id', $this->task['id'])->update($update); + Db::commit(); + } catch (Exception $e) { + Db::rollback(); + throw $e; + } + } + + private function unlockTaskData() + { + Db::name('cert_deploy')->where('id', $this->task['id'])->update(['islock' => 0]); + } + + private function saveLog($txt) + { + if (empty($this->task['processid'])) return; + if (!is_dir(app()->getRuntimePath() . 'log')) mkdir(app()->getRuntimePath() . 'log'); + $file_name = app()->getRuntimePath().'log/'.$this->task['processid'].'.log'; + $file_exists = file_exists($file_name); + file_put_contents($file_name, $txt . PHP_EOL, FILE_APPEND); + if (!$file_exists) { + @chmod($file_name, 0777); + } + if(php_sapi_name() == 'cli'){ + echo $txt . PHP_EOL; + } + } } \ No newline at end of file diff --git a/app/service/CertOrderService.php b/app/service/CertOrderService.php index e532b89..0935ec4 100644 --- a/app/service/CertOrderService.php +++ b/app/service/CertOrderService.php @@ -1,441 +1,441 @@ -where('id', $oid)->find(); - if (!$order) throw new Exception('该证书订单不存在', 102); - $this->order = $order; - - $this->aid = $order['aid']; - $account = Db::name('cert_account')->where('id', $this->aid)->find(); - if (!$account) throw new Exception('该证书账户不存在', 102); - $config = json_decode($account['config'], true); - $ext = $account['ext'] ? json_decode($account['ext'], true) : null; - $this->atype = $account['type']; - $this->client = CertHelper::getModel2($account['type'], $config, $ext); - if (!$this->client) throw new Exception('该证书类型不存在', 102); - - $domainList = Db::name('cert_domain')->where('oid', $oid)->order('sort', 'asc')->column('domain'); - if (!$domainList) throw new Exception('该证书订单没有绑定域名', 102); - $this->domainList = $domainList; - $this->info = $order['info'] ? json_decode($order['info'], true) : null; - $this->dnsList = $order['dns'] ? json_decode($order['dns'], true) : null; - } - - //执行证书申请 - public function process($isManual = false) - { - if ($this->order['status'] >= 3) return 3; - if ($this->order['retry2'] >= 3 && !$isManual) { - throw new Exception('已超出最大重试次数('.$this->order['error'].')', 103); - } - if ($this->order['status'] != 1 && $this->order['status'] != 2 && $this->order['retry'] >= 3 && !$isManual) { - if ($this->order['status'] == -2 || $this->order['status'] == -5 || $this->order['status'] == -6 || $this->order['status'] == -7) { - $this->cancel(); - if($this->order['status'] <= -5) $this->delDns(); - Db::name('cert_order')->where('id', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retrytime' => null, 'updatetime' => date('Y-m-d H:i:s')])->inc('retry2')->update(); - $this->order['status'] = 0; - $this->order['retry'] = 0; - } else { - throw new Exception('已超出最大重试次数('.$this->order['error'].')', 103); - } - } - - $cname = CertHelper::$cert_config[$this->atype]['cname']; - foreach($this->domainList as $domain){ - $mainDomain = getMainDomain($domain); - $drow = Db::name('domain')->where('name', $mainDomain)->find(); - if (!$drow && preg_match('/^xn--/', $mainDomain)) { - $drow = Db::name('domain')->where('name', idn_to_utf8($mainDomain))->find(); - } - if (!$drow) { - if (substr($domain, 0, 2) == '*.') $domain = substr($domain, 2); - $cname_row = Db::name('cert_cname')->where('domain', $domain)->where('status', 1)->find(); - if (!$cname || !$cname_row) { - $errmsg = '域名'.$domain.'未在本系统添加'; - Db::name('cert_order')->where('id', $this->order['id'])->data(['error'=>$errmsg]); - throw new Exception($errmsg, 103); - } else { - $this->cnameDomainList[] = $cname_row['id']; - } - } - } - - $this->lockOrder(); - try { - return $this->processOrder($isManual); - } finally { - $this->unlockOrder(); - if (($this->order['status'] == -2 || $this->order['status'] == -5 || $this->order['status'] == -6 || $this->order['status'] == -7) && $this->order['retry'] >= 3) { - Db::name('cert_order')->where('id', $this->order['id'])->data(['retrytime' => date('Y-m-d H:i:s', time() + 3600)])->update(); - } - } - } - - private function processOrder($isManual = false) - { - $this->client->setLogger(function ($txt) { - $this->saveLog($txt); - }); - // step1: 购买证书 - if ($this->order['status'] == 0 || $this->order['status'] == -1) { - $this->saveLog(date('Y-m-d H:i:s').' - 开始购买证书'); - $this->buyCert(); - } - // step2: 创建订单 - if ($this->order['status'] == 0 || $this->order['status'] == -2) { - $this->saveLog(date('Y-m-d H:i:s').' - 开始创建订单'); - $this->createOrder(); - } - // step3: 添加DNS - if ($isManual && $this->order['status'] == -3 && CertDnsUtils::verifyDns($this->dnsList)) { - $this->saveResult(1); - $this->saveLog('检测到DNS记录已添加成功'); - return 1; - } - if ($this->order['status'] == 0 || $this->order['status'] == -3) { - $this->saveLog(date('Y-m-d H:i:s').' - 开始添加DNS记录'); - $this->addDns(); - $this->saveLog('添加DNS记录成功,请等待生效后进行验证...'); - if (CertHelper::$cert_config[$this->atype]['cname']) { - Db::name('cert_order')->where('id', $this->order['id'])->update(['retrytime' => date('Y-m-d H:i:s', time() + 180)]); - } - return 1; - } - // step4: 查询DNS - if ($this->order['status'] == 1 || $this->order['status'] == -4) { - $this->verifyDns(); - } - // step5: 验证订单 - if ($this->order['status'] == 1 || $this->order['status'] == -5) { - $this->saveLog(date('Y-m-d H:i:s').' - 开始验证订单'); - $this->authOrder(); - } - // step6: 查询验证结果 - if ($this->order['status'] == 2 || $this->order['status'] == -6) { - $this->saveLog(date('Y-m-d H:i:s').' - 开始查询验证结果'); - $this->getAuthStatus(); - } - // step7: 签发证书 - if ($this->order['status'] == 2 || $this->order['status'] == -7) { - $this->saveLog(date('Y-m-d H:i:s').' - 开始签发证书'); - $this->finalizeOrder(); - } - $this->delDns(); - $this->resetRetry2(); - $this->saveLog('[Success] 证书签发成功'); - Db::name('cert_deploy')->where('oid', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retrytime' => null, 'issend' => 0])->update(); - return 3; - } - - private function lockOrder() - { - Db::startTrans(); - try { - $isLock = Db::name('cert_order')->where('id', $this->order['id'])->lock(true)->value('islock'); - if ($isLock == 1 && time() - strtotime($this->order['locktime']) < 3600) { - throw new Exception('订单正在处理中,请稍后再试', 102); - } - $update = ['islock' => 1, 'locktime' => date('Y-m-d H:i:s')]; - if (empty($this->order['processid'])) $this->order['processid'] = $update['processid'] = getSid(); - Db::name('cert_order')->where('id', $this->order['id'])->update($update); - Db::commit(); - } catch (Exception $e) { - Db::rollback(); - throw $e; - } - } - - private function unlockOrder() - { - Db::name('cert_order')->where('id', $this->order['id'])->update(['islock' => 0]); - } - - private function saveResult($status, $error = null, $retrytime = null) - { - $this->order['status'] = $status; - if (!empty($error) && strlen($error) > 300) { - $error = mb_strcut($error, 0, 300); - } - $update = ['status' => $status, 'error' => $error, 'updatetime' => date('Y-m-d H:i:s'), 'retrytime' => $retrytime]; - $res = Db::name('cert_order')->where('id', $this->order['id'])->data($update); - if ($status < 0 || $retrytime) { - $this->order['retry']++; - $res->inc('retry'); - } - $res->update(); - if ($error) { - $this->saveLog('[Error] ' . $error); - } - } - - private function resetRetry() - { - if ($this->order['retry'] > 0) { - $this->order['retry'] = 0; - Db::name('cert_order')->where('id', $this->order['id'])->update(['retry' => 0, 'retrytime' => null]); - } - } - - private function resetRetry2() - { - if ($this->order['retry2'] > 0) { - $this->order['retry2'] = 0; - Db::name('cert_order')->where('id', $this->order['id'])->update(['retry2' => 0, 'retrytime' => null]); - } - } - - //重置订单 - public function reset() - { - Db::name('cert_order')->where('id', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retry2' => 0, 'retrytime' => null, 'processid' => null, 'updatetime' => date('Y-m-d H:i:s'), 'issend' => 0, 'islock' => 0])->update(); - $file_name = app()->getRuntimePath().'log/'.$this->order['processid'].'.log'; - if (file_exists($file_name)) unlink($file_name); - $this->order['status'] = 0; - $this->order['retry'] = 0; - $this->order['retry2'] = 0; - $this->order['processid'] = null; - } - - //购买证书 - public function buyCert() - { - try { - $this->client->buyCert($this->domainList, $this->info); - } catch (Exception $e) { - $this->saveResult(-1, $e->getMessage()); - throw $e; - } - if($this->info){ - Db::name('cert_order')->where('id', $this->order['id'])->update(['info' => json_encode($this->info)]); - } - $this->order['status'] = 0; - $this->resetRetry(); - } - - //创建订单 - public function createOrder() - { - try { - if (!empty($this->cnameDomainList)) { - foreach($this->cnameDomainList as $cnameId){ - $this->checkDomainCname($cnameId); - } - } - try { - $this->dnsList = $this->client->createOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']); - } catch (Exception $e) { - if (strpos($e->getMessage(), 'KeyID header contained an invalid account URL') !== false) { - $ext = $this->client->register(); - Db::name('cert_account')->where('id', $this->aid)->update(['ext' => json_encode($ext)]); - $this->dnsList = $this->client->createOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']); - } else { - throw $e; - } - } - } catch (Exception $e) { - $this->saveResult(-2, $e->getMessage()); - throw $e; - } - Db::name('cert_order')->where('id', $this->order['id'])->update(['info' => json_encode($this->info), 'dns' => json_encode($this->dnsList)]); - - if (!empty($this->dnsList)) { - $dns_txt = '需验证的DNS记录如下:'; - foreach ($this->dnsList as $mainDomain => $list) { - foreach ($list as $row) { - $domain = $row['name'] . '.' . $mainDomain; - $dns_txt .= PHP_EOL.'主机记录: '.$domain.' 类型: '.$row['type'].' 记录值: '.$row['value']; - } - } - $this->saveLog($dns_txt); - } - $this->order['status'] = 0; - $this->resetRetry(); - } - - //验证DNS记录 - public function verifyDns() - { - $verify = CertDnsUtils::verifyDns($this->dnsList); - if (!$verify) { - if ($this->order['retry'] >= 10) { - $this->saveResult(-4, '未查询到DNS解析记录'); - } else { - $this->saveLog('未查询到DNS解析记录(尝试第'.($this->order['retry']+1).'次)'); - $this->saveResult(1, null, date('Y-m-d H:i:s', time() + (array_key_exists($this->order['retry'], self::$retry_interval) ? self::$retry_interval[$this->order['retry']] : 1800))); - } - throw new Exception('未查询到DNS解析记录(尝试第'.($this->order['retry']).'次),请稍后再试'); - } - if($this->order['retry'] == 0 && time() - strtotime($this->order['updatetime']) < 10){ - throw new Exception('请等待'.(10 - (time() - strtotime($this->order['updatetime']))).'秒后再试'); - } - $this->order['status'] = 1; - $this->resetRetry(); - } - - //验证订单 - public function authOrder() - { - try { - $this->client->authOrder($this->domainList, $this->info); - } catch (Exception $e) { - $this->saveResult(-5, $e->getMessage()); - throw $e; - } - $this->saveResult(2); - $this->resetRetry(); - } - - //查询验证结果 - public function getAuthStatus() - { - try { - $status = $this->client->getAuthStatus($this->domainList, $this->info); - } catch (Exception $e) { - $this->saveResult(-6, $e->getMessage()); - throw $e; - } - if(!$status){ - if ($this->order['retry'] >= 10) { - $this->saveResult(-6, '订单验证未通过'); - } else { - $this->saveLog('订单验证未通过(尝试第'.($this->order['retry']+1).'次)'); - $this->saveResult(2, null, date('Y-m-d H:i:s', time() + (array_key_exists($this->order['retry'], self::$retry_interval) ? self::$retry_interval[$this->order['retry']] : 1800))); - } - throw new Exception('订单验证未通过(尝试第'.($this->order['retry']).'次),请稍后再试'); - } - $this->order['status'] = 2; - $this->resetRetry(); - } - - //签发证书 - public function finalizeOrder() - { - try { - $result = $this->client->finalizeOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']); - } catch (Exception $e) { - $this->saveResult(-7, $e->getMessage()); - throw $e; - } - $this->order['issuer'] = $result['issuer']; - Db::name('cert_order')->where('id', $this->order['id'])->update(['fullchain' => $result['fullchain'], 'privatekey' => $result['private_key'], 'issuer' => $result['issuer'], 'issuetime' => date('Y-m-d H:i:s', $result['validFrom']), 'expiretime' => date('Y-m-d H:i:s', $result['validTo'])]); - $this->saveResult(3); - $this->resetRetry(); - } - - //吊销证书 - public function revoke() - { - $this->client->setLogger(function ($txt) { - $this->saveLog($txt); - }); - try { - $this->client->revoke($this->info, $this->order['fullchain']); - } catch (Exception $e) { - throw $e; - } - $this->saveResult(4); - } - - //取消证书订单 - public function cancel(){ - $this->client->setLogger(function ($txt) { - $this->saveLog($txt); - }); - if($this->order['status'] == 1 || $this->order['status'] == 2 || $this->order['status'] < -2){ - try { - $this->client->cancel($this->info); - } catch (Exception $e) { - } - } - } - - //添加DNS记录 - public function addDns() - { - if (empty($this->dnsList)) { - $this->saveResult(1); - return; - } - try { - CertDnsUtils::addDns($this->dnsList, function ($txt) { - $this->saveLog($txt); - }, !empty($this->cnameDomainList)); - } catch (Exception $e) { - $this->saveResult(-3, $e->getMessage()); - throw $e; - } - $this->saveResult(1); - $this->resetRetry(); - } - - //删除DNS记录 - public function delDns() - { - if (empty($this->dnsList)) return; - try { - CertDnsUtils::delDns($this->dnsList, function ($txt) { - $this->saveLog($txt); - }, true); - } catch (Exception $e) { - $this->saveLog('[Error] ' . $e->getMessage()); - } - } - - //检查域名CNAME代理记录 - private function checkDomainCname($id) - { - $row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.id', $id)->field('A.*,B.name cnamedomain')->find(); - $domain = '_acme-challenge.' . $row['domain']; - $record = $row['rr'] . '.' . $row['cnamedomain']; - $result = \app\utils\DnsQueryUtils::get_dns_records($domain, 'CNAME'); - if (!$result || !in_array($record, $result)) { - $result = \app\utils\DnsQueryUtils::query_dns_doh($domain, 'CNAME'); - if (!$result || !in_array($record, $result)) { - if ($row['status'] == 1) { - Db::name('cert_cname')->where('id', $id)->update(['status' => 0]); - } - throw new Exception('域名' . $row['domain'] . '的CNAME代理记录未验证通过'); - } - } - } - - private function saveLog($txt) - { - if (empty($this->order['processid'])) return; - if (!is_dir(app()->getRuntimePath() . 'log')) mkdir(app()->getRuntimePath() . 'log'); - $file_name = app()->getRuntimePath().'log/'.$this->order['processid'].'.log'; - $file_exists = file_exists($file_name); - file_put_contents($file_name, $txt . PHP_EOL, FILE_APPEND); - if (!$file_exists) { - @chmod($file_name, 0777); - } - if(php_sapi_name() == 'cli'){ - echo $txt . PHP_EOL; - } - } -} +where('id', $oid)->find(); + if (!$order) throw new Exception('该证书订单不存在', 102); + $this->order = $order; + + $this->aid = $order['aid']; + $account = Db::name('cert_account')->where('id', $this->aid)->find(); + if (!$account) throw new Exception('该证书账户不存在', 102); + $config = json_decode($account['config'], true); + $ext = $account['ext'] ? json_decode($account['ext'], true) : null; + $this->atype = $account['type']; + $this->client = CertHelper::getModel2($account['type'], $config, $ext); + if (!$this->client) throw new Exception('该证书类型不存在', 102); + + $domainList = Db::name('cert_domain')->where('oid', $oid)->order('sort', 'asc')->column('domain'); + if (!$domainList) throw new Exception('该证书订单没有绑定域名', 102); + $this->domainList = $domainList; + $this->info = $order['info'] ? json_decode($order['info'], true) : null; + $this->dnsList = $order['dns'] ? json_decode($order['dns'], true) : null; + } + + //执行证书申请 + public function process($isManual = false) + { + if ($this->order['status'] >= 3) return 3; + if ($this->order['retry2'] >= 3 && !$isManual) { + throw new Exception('已超出最大重试次数('.$this->order['error'].')', 103); + } + if ($this->order['status'] != 1 && $this->order['status'] != 2 && $this->order['retry'] >= 3 && !$isManual) { + if ($this->order['status'] == -2 || $this->order['status'] == -5 || $this->order['status'] == -6 || $this->order['status'] == -7) { + $this->cancel(); + if($this->order['status'] <= -5) $this->delDns(); + Db::name('cert_order')->where('id', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retrytime' => null, 'updatetime' => date('Y-m-d H:i:s')])->inc('retry2')->update(); + $this->order['status'] = 0; + $this->order['retry'] = 0; + } else { + throw new Exception('已超出最大重试次数('.$this->order['error'].')', 103); + } + } + + $cname = CertHelper::$cert_config[$this->atype]['cname']; + foreach($this->domainList as $domain){ + $mainDomain = getMainDomain($domain); + $drow = Db::name('domain')->where('name', $mainDomain)->find(); + if (!$drow && preg_match('/^xn--/', $mainDomain)) { + $drow = Db::name('domain')->where('name', idn_to_utf8($mainDomain))->find(); + } + if (!$drow) { + if (substr($domain, 0, 2) == '*.') $domain = substr($domain, 2); + $cname_row = Db::name('cert_cname')->where('domain', $domain)->where('status', 1)->find(); + if (!$cname || !$cname_row) { + $errmsg = '域名'.$domain.'未在本系统添加'; + Db::name('cert_order')->where('id', $this->order['id'])->data(['error'=>$errmsg]); + throw new Exception($errmsg, 103); + } else { + $this->cnameDomainList[] = $cname_row['id']; + } + } + } + + $this->lockOrder(); + try { + return $this->processOrder($isManual); + } finally { + $this->unlockOrder(); + if (($this->order['status'] == -2 || $this->order['status'] == -5 || $this->order['status'] == -6 || $this->order['status'] == -7) && $this->order['retry'] >= 3) { + Db::name('cert_order')->where('id', $this->order['id'])->data(['retrytime' => date('Y-m-d H:i:s', time() + 3600)])->update(); + } + } + } + + private function processOrder($isManual = false) + { + $this->client->setLogger(function ($txt) { + $this->saveLog($txt); + }); + // step1: 购买证书 + if ($this->order['status'] == 0 || $this->order['status'] == -1) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始购买证书'); + $this->buyCert(); + } + // step2: 创建订单 + if ($this->order['status'] == 0 || $this->order['status'] == -2) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始创建订单'); + $this->createOrder(); + } + // step3: 添加DNS + if ($isManual && $this->order['status'] == -3 && CertDnsUtils::verifyDns($this->dnsList)) { + $this->saveResult(1); + $this->saveLog('检测到DNS记录已添加成功'); + return 1; + } + if ($this->order['status'] == 0 || $this->order['status'] == -3) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始添加DNS记录'); + $this->addDns(); + $this->saveLog('添加DNS记录成功,请等待生效后进行验证...'); + if (CertHelper::$cert_config[$this->atype]['cname']) { + Db::name('cert_order')->where('id', $this->order['id'])->update(['retrytime' => date('Y-m-d H:i:s', time() + 180)]); + } + return 1; + } + // step4: 查询DNS + if ($this->order['status'] == 1 || $this->order['status'] == -4) { + $this->verifyDns(); + } + // step5: 验证订单 + if ($this->order['status'] == 1 || $this->order['status'] == -5) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始验证订单'); + $this->authOrder(); + } + // step6: 查询验证结果 + if ($this->order['status'] == 2 || $this->order['status'] == -6) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始查询验证结果'); + $this->getAuthStatus(); + } + // step7: 签发证书 + if ($this->order['status'] == 2 || $this->order['status'] == -7) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始签发证书'); + $this->finalizeOrder(); + } + $this->delDns(); + $this->resetRetry2(); + $this->saveLog('[Success] 证书签发成功'); + Db::name('cert_deploy')->where('oid', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retrytime' => null, 'issend' => 0])->update(); + return 3; + } + + private function lockOrder() + { + Db::startTrans(); + try { + $isLock = Db::name('cert_order')->where('id', $this->order['id'])->lock(true)->value('islock'); + if ($isLock == 1 && time() - strtotime($this->order['locktime']) < 3600) { + throw new Exception('订单正在处理中,请稍后再试', 102); + } + $update = ['islock' => 1, 'locktime' => date('Y-m-d H:i:s')]; + if (empty($this->order['processid'])) $this->order['processid'] = $update['processid'] = getSid(); + Db::name('cert_order')->where('id', $this->order['id'])->update($update); + Db::commit(); + } catch (Exception $e) { + Db::rollback(); + throw $e; + } + } + + private function unlockOrder() + { + Db::name('cert_order')->where('id', $this->order['id'])->update(['islock' => 0]); + } + + private function saveResult($status, $error = null, $retrytime = null) + { + $this->order['status'] = $status; + if (!empty($error) && strlen($error) > 300) { + $error = mb_strcut($error, 0, 300); + } + $update = ['status' => $status, 'error' => $error, 'updatetime' => date('Y-m-d H:i:s'), 'retrytime' => $retrytime]; + $res = Db::name('cert_order')->where('id', $this->order['id'])->data($update); + if ($status < 0 || $retrytime) { + $this->order['retry']++; + $res->inc('retry'); + } + $res->update(); + if ($error) { + $this->saveLog('[Error] ' . $error); + } + } + + private function resetRetry() + { + if ($this->order['retry'] > 0) { + $this->order['retry'] = 0; + Db::name('cert_order')->where('id', $this->order['id'])->update(['retry' => 0, 'retrytime' => null]); + } + } + + private function resetRetry2() + { + if ($this->order['retry2'] > 0) { + $this->order['retry2'] = 0; + Db::name('cert_order')->where('id', $this->order['id'])->update(['retry2' => 0, 'retrytime' => null]); + } + } + + //重置订单 + public function reset() + { + Db::name('cert_order')->where('id', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retry2' => 0, 'retrytime' => null, 'processid' => null, 'updatetime' => date('Y-m-d H:i:s'), 'issend' => 0, 'islock' => 0])->update(); + $file_name = app()->getRuntimePath().'log/'.$this->order['processid'].'.log'; + if (file_exists($file_name)) unlink($file_name); + $this->order['status'] = 0; + $this->order['retry'] = 0; + $this->order['retry2'] = 0; + $this->order['processid'] = null; + } + + //购买证书 + public function buyCert() + { + try { + $this->client->buyCert($this->domainList, $this->info); + } catch (Exception $e) { + $this->saveResult(-1, $e->getMessage()); + throw $e; + } + if($this->info){ + Db::name('cert_order')->where('id', $this->order['id'])->update(['info' => json_encode($this->info)]); + } + $this->order['status'] = 0; + $this->resetRetry(); + } + + //创建订单 + public function createOrder() + { + try { + if (!empty($this->cnameDomainList)) { + foreach($this->cnameDomainList as $cnameId){ + $this->checkDomainCname($cnameId); + } + } + try { + $this->dnsList = $this->client->createOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']); + } catch (Exception $e) { + if (strpos($e->getMessage(), 'KeyID header contained an invalid account URL') !== false) { + $ext = $this->client->register(); + Db::name('cert_account')->where('id', $this->aid)->update(['ext' => json_encode($ext)]); + $this->dnsList = $this->client->createOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']); + } else { + throw $e; + } + } + } catch (Exception $e) { + $this->saveResult(-2, $e->getMessage()); + throw $e; + } + Db::name('cert_order')->where('id', $this->order['id'])->update(['info' => json_encode($this->info), 'dns' => json_encode($this->dnsList)]); + + if (!empty($this->dnsList)) { + $dns_txt = '需验证的DNS记录如下:'; + foreach ($this->dnsList as $mainDomain => $list) { + foreach ($list as $row) { + $domain = $row['name'] . '.' . $mainDomain; + $dns_txt .= PHP_EOL.'主机记录: '.$domain.' 类型: '.$row['type'].' 记录值: '.$row['value']; + } + } + $this->saveLog($dns_txt); + } + $this->order['status'] = 0; + $this->resetRetry(); + } + + //验证DNS记录 + public function verifyDns() + { + $verify = CertDnsUtils::verifyDns($this->dnsList); + if (!$verify) { + if ($this->order['retry'] >= 10) { + $this->saveResult(-4, '未查询到DNS解析记录'); + } else { + $this->saveLog('未查询到DNS解析记录(尝试第'.($this->order['retry']+1).'次)'); + $this->saveResult(1, null, date('Y-m-d H:i:s', time() + (array_key_exists($this->order['retry'], self::$retry_interval) ? self::$retry_interval[$this->order['retry']] : 1800))); + } + throw new Exception('未查询到DNS解析记录(尝试第'.($this->order['retry']).'次),请稍后再试'); + } + if($this->order['retry'] == 0 && time() - strtotime($this->order['updatetime']) < 10){ + throw new Exception('请等待'.(10 - (time() - strtotime($this->order['updatetime']))).'秒后再试'); + } + $this->order['status'] = 1; + $this->resetRetry(); + } + + //验证订单 + public function authOrder() + { + try { + $this->client->authOrder($this->domainList, $this->info); + } catch (Exception $e) { + $this->saveResult(-5, $e->getMessage()); + throw $e; + } + $this->saveResult(2); + $this->resetRetry(); + } + + //查询验证结果 + public function getAuthStatus() + { + try { + $status = $this->client->getAuthStatus($this->domainList, $this->info); + } catch (Exception $e) { + $this->saveResult(-6, $e->getMessage()); + throw $e; + } + if(!$status){ + if ($this->order['retry'] >= 10) { + $this->saveResult(-6, '订单验证未通过'); + } else { + $this->saveLog('订单验证未通过(尝试第'.($this->order['retry']+1).'次)'); + $this->saveResult(2, null, date('Y-m-d H:i:s', time() + (array_key_exists($this->order['retry'], self::$retry_interval) ? self::$retry_interval[$this->order['retry']] : 1800))); + } + throw new Exception('订单验证未通过(尝试第'.($this->order['retry']).'次),请稍后再试'); + } + $this->order['status'] = 2; + $this->resetRetry(); + } + + //签发证书 + public function finalizeOrder() + { + try { + $result = $this->client->finalizeOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']); + } catch (Exception $e) { + $this->saveResult(-7, $e->getMessage()); + throw $e; + } + $this->order['issuer'] = $result['issuer']; + Db::name('cert_order')->where('id', $this->order['id'])->update(['fullchain' => $result['fullchain'], 'privatekey' => $result['private_key'], 'issuer' => $result['issuer'], 'issuetime' => date('Y-m-d H:i:s', $result['validFrom']), 'expiretime' => date('Y-m-d H:i:s', $result['validTo'])]); + $this->saveResult(3); + $this->resetRetry(); + } + + //吊销证书 + public function revoke() + { + $this->client->setLogger(function ($txt) { + $this->saveLog($txt); + }); + try { + $this->client->revoke($this->info, $this->order['fullchain']); + } catch (Exception $e) { + throw $e; + } + $this->saveResult(4); + } + + //取消证书订单 + public function cancel(){ + $this->client->setLogger(function ($txt) { + $this->saveLog($txt); + }); + if($this->order['status'] == 1 || $this->order['status'] == 2 || $this->order['status'] < -2){ + try { + $this->client->cancel($this->info); + } catch (Exception $e) { + } + } + } + + //添加DNS记录 + public function addDns() + { + if (empty($this->dnsList)) { + $this->saveResult(1); + return; + } + try { + CertDnsUtils::addDns($this->dnsList, function ($txt) { + $this->saveLog($txt); + }, !empty($this->cnameDomainList)); + } catch (Exception $e) { + $this->saveResult(-3, $e->getMessage()); + throw $e; + } + $this->saveResult(1); + $this->resetRetry(); + } + + //删除DNS记录 + public function delDns() + { + if (empty($this->dnsList)) return; + try { + CertDnsUtils::delDns($this->dnsList, function ($txt) { + $this->saveLog($txt); + }, true); + } catch (Exception $e) { + $this->saveLog('[Error] ' . $e->getMessage()); + } + } + + //检查域名CNAME代理记录 + private function checkDomainCname($id) + { + $row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.id', $id)->field('A.*,B.name cnamedomain')->find(); + $domain = '_acme-challenge.' . $row['domain']; + $record = $row['rr'] . '.' . $row['cnamedomain']; + $result = \app\utils\DnsQueryUtils::get_dns_records($domain, 'CNAME'); + if (!$result || !in_array($record, $result)) { + $result = \app\utils\DnsQueryUtils::query_dns_doh($domain, 'CNAME'); + if (!$result || !in_array($record, $result)) { + if ($row['status'] == 1) { + Db::name('cert_cname')->where('id', $id)->update(['status' => 0]); + } + throw new Exception('域名' . $row['domain'] . '的CNAME代理记录未验证通过'); + } + } + } + + private function saveLog($txt) + { + if (empty($this->order['processid'])) return; + if (!is_dir(app()->getRuntimePath() . 'log')) mkdir(app()->getRuntimePath() . 'log'); + $file_name = app()->getRuntimePath().'log/'.$this->order['processid'].'.log'; + $file_exists = file_exists($file_name); + file_put_contents($file_name, $txt . PHP_EOL, FILE_APPEND); + if (!$file_exists) { + @chmod($file_name, 0777); + } + if(php_sapi_name() == 'cli'){ + echo $txt . PHP_EOL; + } + } +} diff --git a/app/service/CertTaskService.php b/app/service/CertTaskService.php index e2bf9d1..accae2b 100644 --- a/app/service/CertTaskService.php +++ b/app/service/CertTaskService.php @@ -1,104 +1,104 @@ -execute_deploy()) { - config_set('certdeploy_time', date("Y-m-d H:i:s")); - } - if ($this->execute_order()) { - config_set('certtask_time', date("Y-m-d H:i:s")); - } - } - - private function execute_order() - { - echo '开始执行SSL证书签发任务...'.PHP_EOL; - $days = config_get('cert_renewdays', 7); - $list = Db::name('cert_order')->field('id,aid,status,issend')->whereRaw('status NOT IN (3,4) AND (retrytime IS NULL OR retrytime date('Y-m-d H:i:s', time() + $days * 86400)])->select(); - //print_r($list);exit; - $failcount = 0; - foreach ($list as $row) { - if ($row['aid'] == 0) { - if($row['issend'] == 0) MsgNotice::cert_order_send($row['id'], true); - continue; - } - try { - $service = new CertOrderService($row['id']); - if ($row['status'] == 3) { - $service->reset(); - } - $retcode = $service->process(); - if ($retcode == 3) { - echo 'ID:'.$row['id'].' 证书已签发成功!'.PHP_EOL; - if($row['issend'] == 0) MsgNotice::cert_order_send($row['id'], true); - } elseif ($retcode == 1) { - echo 'ID:'.$row['id'].' 添加DNS记录成功!'.PHP_EOL; - } - break; - } catch (Exception $e) { - echo 'ID:'.$row['id'].' '.$e->getMessage().PHP_EOL; - if ($e->getCode() == 102) { - break; - } elseif ($e->getCode() == 103) { - if($row['issend'] == 0) MsgNotice::cert_order_send($row['id'], false); - } else { - $failcount++; - } - } - if ($failcount >= 3) break; - sleep(1); - } - return true; - } - - private function execute_deploy() - { - $start = config_get('deploy_hour_start', 0); - $end = config_get('deploy_hour_end', 23); - $hour = date('H'); - if($start <= $end){ - if($hour < $start || $hour > $end){ - echo '不在部署任务运行时间范围内'.PHP_EOL; return false; - } - }else{ - if($hour < $start && $hour > $end){ - echo '不在部署任务运行时间范围内'.PHP_EOL; return false; - } - } - - echo '开始执行SSL证书部署任务...'.PHP_EOL; - $list = Db::name('cert_deploy')->field('id,status,issend')->whereRaw('active=1 AND status IN (0,-1) AND (retrytime IS NULL OR retrytimeselect(); - //print_r($list);exit; - $count = 0; - foreach ($list as $row) { - try { - $service = new CertDeployService($row['id']); - $service->process(); - echo 'ID:'.$row['id'].' 部署任务执行成功!'.PHP_EOL; - if($row['issend'] == 0) MsgNotice::cert_deploy_send($row['id'], true); - $count++; - } catch (Exception $e) { - echo 'ID:'.$row['id'].' '.$e->getMessage().PHP_EOL; - if ($e->getCode() == 102) { - break; - } elseif ($e->getCode() == 103) { - if($row['issend'] == 0) MsgNotice::cert_deploy_send($row['id'], false); - } else { - $count++; - } - } - if ($count >= 3) break; - sleep(1); - } - return true; - } -} +execute_deploy()) { + config_set('certdeploy_time', date("Y-m-d H:i:s")); + } + if ($this->execute_order()) { + config_set('certtask_time', date("Y-m-d H:i:s")); + } + } + + private function execute_order() + { + echo '开始执行SSL证书签发任务...'.PHP_EOL; + $days = config_get('cert_renewdays', 7); + $list = Db::name('cert_order')->field('id,aid,status,issend')->whereRaw('status NOT IN (3,4) AND (retrytime IS NULL OR retrytime date('Y-m-d H:i:s', time() + $days * 86400)])->select(); + //print_r($list);exit; + $failcount = 0; + foreach ($list as $row) { + if ($row['aid'] == 0) { + if($row['issend'] == 0) MsgNotice::cert_order_send($row['id'], true); + continue; + } + try { + $service = new CertOrderService($row['id']); + if ($row['status'] == 3) { + $service->reset(); + } + $retcode = $service->process(); + if ($retcode == 3) { + echo 'ID:'.$row['id'].' 证书已签发成功!'.PHP_EOL; + if($row['issend'] == 0) MsgNotice::cert_order_send($row['id'], true); + } elseif ($retcode == 1) { + echo 'ID:'.$row['id'].' 添加DNS记录成功!'.PHP_EOL; + } + break; + } catch (Exception $e) { + echo 'ID:'.$row['id'].' '.$e->getMessage().PHP_EOL; + if ($e->getCode() == 102) { + break; + } elseif ($e->getCode() == 103) { + if($row['issend'] == 0) MsgNotice::cert_order_send($row['id'], false); + } else { + $failcount++; + } + } + if ($failcount >= 3) break; + sleep(1); + } + return true; + } + + private function execute_deploy() + { + $start = config_get('deploy_hour_start', 0); + $end = config_get('deploy_hour_end', 23); + $hour = date('H'); + if($start <= $end){ + if($hour < $start || $hour > $end){ + echo '不在部署任务运行时间范围内'.PHP_EOL; return false; + } + }else{ + if($hour < $start && $hour > $end){ + echo '不在部署任务运行时间范围内'.PHP_EOL; return false; + } + } + + echo '开始执行SSL证书部署任务...'.PHP_EOL; + $list = Db::name('cert_deploy')->field('id,status,issend')->whereRaw('active=1 AND status IN (0,-1) AND (retrytime IS NULL OR retrytimeselect(); + //print_r($list);exit; + $count = 0; + foreach ($list as $row) { + try { + $service = new CertDeployService($row['id']); + $service->process(); + echo 'ID:'.$row['id'].' 部署任务执行成功!'.PHP_EOL; + if($row['issend'] == 0) MsgNotice::cert_deploy_send($row['id'], true); + $count++; + } catch (Exception $e) { + echo 'ID:'.$row['id'].' '.$e->getMessage().PHP_EOL; + if ($e->getCode() == 102) { + break; + } elseif ($e->getCode() == 103) { + if($row['issend'] == 0) MsgNotice::cert_deploy_send($row['id'], false); + } else { + $count++; + } + } + if ($count >= 3) break; + sleep(1); + } + return true; + } +} diff --git a/app/service/ExpireNoticeService.php b/app/service/ExpireNoticeService.php index e541896..85e1a9a 100644 --- a/app/service/ExpireNoticeService.php +++ b/app/service/ExpireNoticeService.php @@ -1,104 +1,104 @@ -where('id', $id)->update(['regtime' => $regTime, 'expiretime' => $expireTime, 'checktime' => date('Y-m-d H:i:s'), 'checkstatus' => 1]); - return ['code' => 0, 'regTime' => $regTime, 'expireTime' => $expireTime, 'msg' => 'Success']; - } catch (Exception $e) { - Db::name('domain')->where('id', $id)->update(['checktime' => date('Y-m-d H:i:s'), 'checkstatus' => 2]); - return ['code' => -1, 'msg' => $e->getMessage()]; - } - } - - public function task() - { - echo '开始执行域名到期提醒任务...' . PHP_EOL; - config_set('domain_expire_time', date("Y-m-d H:i:s")); - $count = $this->refreshDomainList(); - if ($count > 0) return; - - $days = config_get('expire_noticedays'); - $max_day = 30; - if (!empty($days)) { - $days = explode(',', $days); - $days = array_map('intval', $days); - $max_day = max($days) + 1; - } - $count = $this->refreshExpiringDomainList($max_day); - if ($count > 0) return; - - if (!empty($days) && (config_get('expire_notice_mail') == '1' || config_get('expire_notice_wxtpl') == '1' || config_get('expire_notice_tgbot') == '1' || config_get('expire_notice_webhook') == '1') && date('H') >= 9) { - $this->noticeExpiringDomainList($max_day, $days); - } - } - - private function refreshDomainList() - { - $domainList = Db::name('domain')->field('id,name')->where('expiretime', null)->where('checkstatus', 0)->select(); - $count = 0; - foreach ($domainList as $domain) { - $res = $this->updateDomainDate($domain['id'], $domain['name']); - if ($res['code'] == 0) { - echo '域名: ' . $domain['name'] . ' 注册时间: ' . $res['regTime'] . ' 到期时间: ' . $res['expireTime'] . PHP_EOL; - } else { - echo '域名: ' . $domain['name'] . ' 更新失败,' . $res['msg'] . PHP_EOL; - } - $count++; - if ($count >= 5) break; - sleep(1); - } - return $count; - } - - private function refreshExpiringDomainList($max_day) - { - $domainList = Db::name('domain')->field('id,name')->whereRaw('expiretime>=(NOW() - INTERVAL 5 DAY) AND expiretime<=(NOW() + INTERVAL ' . $max_day . ' DAY) AND checktime<=(NOW() - INTERVAL 1 DAY)')->select(); - $count = 0; - foreach ($domainList as $domain) { - $res = $this->updateDomainDate($domain['id'], $domain['name']); - if ($res['code'] == 0) { - echo '域名: ' . $domain['name'] . ' 注册时间: ' . $res['regTime'] . ' 到期时间: ' . $res['expireTime'] . PHP_EOL; - } else { - echo '域名: ' . $domain['name'] . ' 更新失败,' . $res['msg'] . PHP_EOL; - } - $count++; - if ($count >= 5) break; - sleep(1); - } - return $count; - } - - private function noticeExpiringDomainList($max_day, $days) - { - $domainList = Db::name('domain')->field('id,name,expiretime')->whereRaw('expiretime>=NOW() AND expiretime<=(NOW() + INTERVAL ' . $max_day . ' DAY) AND is_notice=1 AND (noticetime IS NULL OR noticetime<=(NOW() - INTERVAL 20 HOUR))')->order('expiretime', 'asc')->select(); - $noticeList = []; - foreach ($domainList as $domain) { - $expireDay = intval((strtotime($domain['expiretime']) - time()) / 86400); - if (in_array($expireDay, $days)) { - $noticeList[$expireDay][] = ['id' => $domain['id'], 'name' => $domain['name'], 'expiretime' => $domain['expiretime']]; - } - } - if (!empty($noticeList)) { - foreach ($noticeList as $day => $list) { - $ids = array_column($list, 'id'); - Db::name('domain')->whereIn('id', $ids)->update(['noticetime' => date('Y-m-d H:i:s')]); - MsgNotice::expire_notice_send($day, $list); - echo '域名到期提醒: ' . $day . '天内到期的' . count($ids) . '个域名已发送' . PHP_EOL; - } - } - } -} +where('id', $id)->update(['regtime' => $regTime, 'expiretime' => $expireTime, 'checktime' => date('Y-m-d H:i:s'), 'checkstatus' => 1]); + return ['code' => 0, 'regTime' => $regTime, 'expireTime' => $expireTime, 'msg' => 'Success']; + } catch (Exception $e) { + Db::name('domain')->where('id', $id)->update(['checktime' => date('Y-m-d H:i:s'), 'checkstatus' => 2]); + return ['code' => -1, 'msg' => $e->getMessage()]; + } + } + + public function task() + { + echo '开始执行域名到期提醒任务...' . PHP_EOL; + config_set('domain_expire_time', date("Y-m-d H:i:s")); + $count = $this->refreshDomainList(); + if ($count > 0) return; + + $days = config_get('expire_noticedays'); + $max_day = 30; + if (!empty($days)) { + $days = explode(',', $days); + $days = array_map('intval', $days); + $max_day = max($days) + 1; + } + $count = $this->refreshExpiringDomainList($max_day); + if ($count > 0) return; + + if (!empty($days) && (config_get('expire_notice_mail') == '1' || config_get('expire_notice_wxtpl') == '1' || config_get('expire_notice_tgbot') == '1' || config_get('expire_notice_webhook') == '1') && date('H') >= 9) { + $this->noticeExpiringDomainList($max_day, $days); + } + } + + private function refreshDomainList() + { + $domainList = Db::name('domain')->field('id,name')->where('expiretime', null)->where('checkstatus', 0)->select(); + $count = 0; + foreach ($domainList as $domain) { + $res = $this->updateDomainDate($domain['id'], $domain['name']); + if ($res['code'] == 0) { + echo '域名: ' . $domain['name'] . ' 注册时间: ' . $res['regTime'] . ' 到期时间: ' . $res['expireTime'] . PHP_EOL; + } else { + echo '域名: ' . $domain['name'] . ' 更新失败,' . $res['msg'] . PHP_EOL; + } + $count++; + if ($count >= 5) break; + sleep(1); + } + return $count; + } + + private function refreshExpiringDomainList($max_day) + { + $domainList = Db::name('domain')->field('id,name')->whereRaw('expiretime>=(NOW() - INTERVAL 5 DAY) AND expiretime<=(NOW() + INTERVAL ' . $max_day . ' DAY) AND checktime<=(NOW() - INTERVAL 1 DAY)')->select(); + $count = 0; + foreach ($domainList as $domain) { + $res = $this->updateDomainDate($domain['id'], $domain['name']); + if ($res['code'] == 0) { + echo '域名: ' . $domain['name'] . ' 注册时间: ' . $res['regTime'] . ' 到期时间: ' . $res['expireTime'] . PHP_EOL; + } else { + echo '域名: ' . $domain['name'] . ' 更新失败,' . $res['msg'] . PHP_EOL; + } + $count++; + if ($count >= 5) break; + sleep(1); + } + return $count; + } + + private function noticeExpiringDomainList($max_day, $days) + { + $domainList = Db::name('domain')->field('id,name,expiretime')->whereRaw('expiretime>=NOW() AND expiretime<=(NOW() + INTERVAL ' . $max_day . ' DAY) AND is_notice=1 AND (noticetime IS NULL OR noticetime<=(NOW() - INTERVAL 20 HOUR))')->order('expiretime', 'asc')->select(); + $noticeList = []; + foreach ($domainList as $domain) { + $expireDay = intval((strtotime($domain['expiretime']) - time()) / 86400); + if (in_array($expireDay, $days)) { + $noticeList[$expireDay][] = ['id' => $domain['id'], 'name' => $domain['name'], 'expiretime' => $domain['expiretime']]; + } + } + if (!empty($noticeList)) { + foreach ($noticeList as $day => $list) { + $ids = array_column($list, 'id'); + Db::name('domain')->whereIn('id', $ids)->update(['noticetime' => date('Y-m-d H:i:s')]); + MsgNotice::expire_notice_send($day, $list); + echo '域名到期提醒: ' . $day . '天内到期的' . count($ids) . '个域名已发送' . PHP_EOL; + } + } + } +} diff --git a/app/service/OptimizeService.php b/app/service/OptimizeService.php index 64d6f9b..02b3ca5 100644 --- a/app/service/OptimizeService.php +++ b/app/service/OptimizeService.php @@ -1,250 +1,250 @@ - config_get('optimize_ip_key', 'o1zrmHAF'), - 'type' => $ip_type, - ]; - $response = get_curl($url, json_encode($params), 0, 0, 0, 0, ['Content-Type' => 'application/json; charset=UTF-8']); - $arr = json_decode($response, true); - if (isset($arr['code']) && $arr['code'] == 200) { - return $arr['info']; - } elseif (isset($arr['info'])) { - throw new Exception('获取优选IP数据失败,'.$arr['info']); - } elseif (isset($arr['msg'])) { - throw new Exception('获取优选IP数据失败,'.$arr['msg']); - } else { - throw new Exception('获取优选IP数据失败,原因未知'); - } - } - - public function get_ip_address2($cdn_type = 1, $ip_type = 'v4') - { - $key = $cdn_type.'_'.$ip_type; - if (!isset($this->ip_address[$key])) { - $info = $this->get_ip_address($cdn_type, $ip_type); - $res = []; - if (isset($info['DEF'])) { - $res['DEF'] = $info['DEF']; - } - if (isset($info['CT'])) { - $res['CT'] = $info['CT']; - } - if (isset($info['CU'])) { - $res['CU'] = $info['CU']; - } - if (isset($info['CM'])) { - $res['CM'] = $info['CM']; - } - $this->ip_address[$key] = $res; - } - return $this->ip_address[$key]; - } - - //批量执行优选任务 - public function execute() - { - $minute = config_get('optimize_ip_min', '30'); - $last = config_get('optimize_ip_time', null, true); - if ($last && strtotime($last) > time() - $minute * 60) { - return false; - } - $list = Db::name('optimizeip')->where('active', 1)->select(); - if (count($list) == 0) { - return false; - } - echo '开始执行IP优选任务,共获取到'.count($list).'个待执行任务'."\n"; - foreach ($list as $row) { - try { - $result = $this->execute_one($row); - Db::name('optimizeip')->where('id', $row['id'])->update(['status' => 1, 'errmsg' => null, 'updatetime' => date('Y-m-d H:i:s')]); - echo '优选任务'.$row['id'].'执行成功:'.$result."\n"; - } catch (Exception $e) { - Db::name('optimizeip')->where('id', $row['id'])->update(['status' => 2, 'errmsg' => $e->getMessage(), 'updatetime' => date('Y-m-d H:i:s')]); - echo '优选任务'.$row['id'].'执行失败:'.$e->getMessage()."\n"; - } - } - config_set('optimize_ip_time', date("Y-m-d H:i:s")); - return true; - } - - //执行单个优选任务 - public function execute_one($row) - { - $this->add_num = 0; - $this->change_num = 0; - $this->del_num = 0; - $ip_types = explode(',', $row['ip_type']); - foreach ($ip_types as $ip_type) { - if (empty($ip_type)) { - 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(); - if (!$drow) { - throw new Exception('域名不存在(ID:'.$row['did'].')'); - } - if (!isset(DnsHelper::$line_name[$drow['type']])) { - throw new Exception('不支持的DNS服务商'); - } - - $info = $this->get_ip_address2($row['cdn_type'], $ip_type); - - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - $domainRecords = $dns->getSubDomainRecords($row['rr'], 1, 100); - if (!$domainRecords) { - throw new Exception('获取记录列表失败,'.$dns->getError()); - } - - if ($row['type'] == 1 && isset($info['DEF']) && !empty($info['DEF'])) { - $row['type'] = 0; - } - - foreach ($info as $line => $iplist) { - if (empty($iplist)) { - continue; - } - $record_num = $row['recordnum']; - $get_ips = array_column($iplist, 'ip'); - if ($drow['type'] == 'huawei') { - sort($get_ips); - $get_ips = array_slice($get_ips, 0, $row['recordnum']); - $get_ips = [implode(',', $get_ips)]; - $record_num = 1; - } - if ($row['type'] == 1 && $line == 'CT') { - $line = 'DEF'; - } - if (!isset(DnsHelper::$line_name[$drow['type']][$line])) { - continue; - } - $line_name = DnsHelper::$line_name[$drow['type']][$line]; - $this->process_dns_line($dns, $row, $domainRecords['list'], $record_num, $get_ips, $line_name, $ip_type); - } - } - - return '成功添加'.$this->add_num.'条记录,修改'.$this->change_num.'条记录,删除'.$this->del_num.'条记录'; - } - - //处理单个线路的解析记录 - private function process_dns_line($dns, $row, $record_list, $record_num, $get_ips, $line_name, $ip_type) - { - $records = array_filter($record_list, function ($v) use ($line_name) { - return $v['Line'] == $line_name; - }); - - //删除CNAME记录 - $cname_records = array_filter($records, function ($v) { - return $v['Type'] == 'CNAME'; - }); - if (!empty($cname_records)) { - foreach ($cname_records as $record) { - $dns->deleteDomainRecord($record['RecordId']); - } - } - - //处理A/AAAA记录 - $ip_records = array_filter($records, function ($v) use ($ip_type) { - return $v['Type'] == ($ip_type == 'v6' ? 'AAAA' : 'A'); - }); - - if (!empty($ip_records) && is_array($ip_records[array_key_first($ip_records)]['Value'])) { //处理华为云记录 - foreach ($ip_records as &$ip_record) { - sort($ip_record['Value']); - $ip_record['Value'] = implode(',', $ip_record['Value']); - } - } - - $exist_ips = array_column($ip_records, 'Value'); - $add_ips = array_diff($get_ips, $exist_ips); - $del_ips = array_diff($exist_ips, $get_ips); - $correct_ips = array_diff($exist_ips, $del_ips); - $correct_count = count($correct_ips); - if (!empty($del_ips)) { - foreach ($ip_records as $record) { - if (in_array($record['Value'], $del_ips)) { - $add_ip = array_pop($add_ips); - if ($add_ip) { - $res = $dns->updateDomainRecord($record['RecordId'], $row['rr'], $ip_type == 'v6' ? 'AAAA' : 'A', $add_ip, $line_name, $row['ttl']); - if (!$res) { - throw new Exception('修改解析失败,'.$dns->getError()); - } - $this->change_num++; - $correct_count++; - } else { - $res = $dns->deleteDomainRecord($record['RecordId']); - if (!$res) { - throw new Exception('删除解析失败,'.$dns->getError()); - } - $this->del_num++; - } - } - } - } - if ($correct_count < $record_num && !empty($add_ips)) { - foreach ($add_ips as $add_ip) { - $res = $dns->addDomainRecord($row['rr'], $ip_type == 'v6' ? 'AAAA' : 'A', $add_ip, $line_name, $row['ttl']); - if (!$res) { - throw new Exception('添加解析失败,'.$dns->getError()); - } - $this->add_num++; - $correct_count++; - if ($correct_count >= $record_num) { - break; - } - } - } - } -} + config_get('optimize_ip_key', 'o1zrmHAF'), + 'type' => $ip_type, + ]; + $response = get_curl($url, json_encode($params), 0, 0, 0, 0, ['Content-Type' => 'application/json; charset=UTF-8']); + $arr = json_decode($response, true); + if (isset($arr['code']) && $arr['code'] == 200) { + return $arr['info']; + } elseif (isset($arr['info'])) { + throw new Exception('获取优选IP数据失败,'.$arr['info']); + } elseif (isset($arr['msg'])) { + throw new Exception('获取优选IP数据失败,'.$arr['msg']); + } else { + throw new Exception('获取优选IP数据失败,原因未知'); + } + } + + public function get_ip_address2($cdn_type = 1, $ip_type = 'v4') + { + $key = $cdn_type.'_'.$ip_type; + if (!isset($this->ip_address[$key])) { + $info = $this->get_ip_address($cdn_type, $ip_type); + $res = []; + if (isset($info['DEF'])) { + $res['DEF'] = $info['DEF']; + } + if (isset($info['CT'])) { + $res['CT'] = $info['CT']; + } + if (isset($info['CU'])) { + $res['CU'] = $info['CU']; + } + if (isset($info['CM'])) { + $res['CM'] = $info['CM']; + } + $this->ip_address[$key] = $res; + } + return $this->ip_address[$key]; + } + + //批量执行优选任务 + public function execute() + { + $minute = config_get('optimize_ip_min', '30'); + $last = config_get('optimize_ip_time', null, true); + if ($last && strtotime($last) > time() - $minute * 60) { + return false; + } + $list = Db::name('optimizeip')->where('active', 1)->select(); + if (count($list) == 0) { + return false; + } + echo '开始执行IP优选任务,共获取到'.count($list).'个待执行任务'."\n"; + foreach ($list as $row) { + try { + $result = $this->execute_one($row); + Db::name('optimizeip')->where('id', $row['id'])->update(['status' => 1, 'errmsg' => null, 'updatetime' => date('Y-m-d H:i:s')]); + echo '优选任务'.$row['id'].'执行成功:'.$result."\n"; + } catch (Exception $e) { + Db::name('optimizeip')->where('id', $row['id'])->update(['status' => 2, 'errmsg' => $e->getMessage(), 'updatetime' => date('Y-m-d H:i:s')]); + echo '优选任务'.$row['id'].'执行失败:'.$e->getMessage()."\n"; + } + } + config_set('optimize_ip_time', date("Y-m-d H:i:s")); + return true; + } + + //执行单个优选任务 + public function execute_one($row) + { + $this->add_num = 0; + $this->change_num = 0; + $this->del_num = 0; + $ip_types = explode(',', $row['ip_type']); + foreach ($ip_types as $ip_type) { + if (empty($ip_type)) { + 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(); + if (!$drow) { + throw new Exception('域名不存在(ID:'.$row['did'].')'); + } + if (!isset(DnsHelper::$line_name[$drow['type']])) { + throw new Exception('不支持的DNS服务商'); + } + + $info = $this->get_ip_address2($row['cdn_type'], $ip_type); + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + $domainRecords = $dns->getSubDomainRecords($row['rr'], 1, 100); + if (!$domainRecords) { + throw new Exception('获取记录列表失败,'.$dns->getError()); + } + + if ($row['type'] == 1 && isset($info['DEF']) && !empty($info['DEF'])) { + $row['type'] = 0; + } + + foreach ($info as $line => $iplist) { + if (empty($iplist)) { + continue; + } + $record_num = $row['recordnum']; + $get_ips = array_column($iplist, 'ip'); + if ($drow['type'] == 'huawei') { + sort($get_ips); + $get_ips = array_slice($get_ips, 0, $row['recordnum']); + $get_ips = [implode(',', $get_ips)]; + $record_num = 1; + } + if ($row['type'] == 1 && $line == 'CT') { + $line = 'DEF'; + } + if (!isset(DnsHelper::$line_name[$drow['type']][$line])) { + continue; + } + $line_name = DnsHelper::$line_name[$drow['type']][$line]; + $this->process_dns_line($dns, $row, $domainRecords['list'], $record_num, $get_ips, $line_name, $ip_type); + } + } + + return '成功添加'.$this->add_num.'条记录,修改'.$this->change_num.'条记录,删除'.$this->del_num.'条记录'; + } + + //处理单个线路的解析记录 + private function process_dns_line($dns, $row, $record_list, $record_num, $get_ips, $line_name, $ip_type) + { + $records = array_filter($record_list, function ($v) use ($line_name) { + return $v['Line'] == $line_name; + }); + + //删除CNAME记录 + $cname_records = array_filter($records, function ($v) { + return $v['Type'] == 'CNAME'; + }); + if (!empty($cname_records)) { + foreach ($cname_records as $record) { + $dns->deleteDomainRecord($record['RecordId']); + } + } + + //处理A/AAAA记录 + $ip_records = array_filter($records, function ($v) use ($ip_type) { + return $v['Type'] == ($ip_type == 'v6' ? 'AAAA' : 'A'); + }); + + if (!empty($ip_records) && is_array($ip_records[array_key_first($ip_records)]['Value'])) { //处理华为云记录 + foreach ($ip_records as &$ip_record) { + sort($ip_record['Value']); + $ip_record['Value'] = implode(',', $ip_record['Value']); + } + } + + $exist_ips = array_column($ip_records, 'Value'); + $add_ips = array_diff($get_ips, $exist_ips); + $del_ips = array_diff($exist_ips, $get_ips); + $correct_ips = array_diff($exist_ips, $del_ips); + $correct_count = count($correct_ips); + if (!empty($del_ips)) { + foreach ($ip_records as $record) { + if (in_array($record['Value'], $del_ips)) { + $add_ip = array_pop($add_ips); + if ($add_ip) { + $res = $dns->updateDomainRecord($record['RecordId'], $row['rr'], $ip_type == 'v6' ? 'AAAA' : 'A', $add_ip, $line_name, $row['ttl']); + if (!$res) { + throw new Exception('修改解析失败,'.$dns->getError()); + } + $this->change_num++; + $correct_count++; + } else { + $res = $dns->deleteDomainRecord($record['RecordId']); + if (!$res) { + throw new Exception('删除解析失败,'.$dns->getError()); + } + $this->del_num++; + } + } + } + } + if ($correct_count < $record_num && !empty($add_ips)) { + foreach ($add_ips as $add_ip) { + $res = $dns->addDomainRecord($row['rr'], $ip_type == 'v6' ? 'AAAA' : 'A', $add_ip, $line_name, $row['ttl']); + if (!$res) { + throw new Exception('添加解析失败,'.$dns->getError()); + } + $this->add_num++; + $correct_count++; + if ($correct_count >= $record_num) { + break; + } + } + } + } +} diff --git a/app/service/ScheduleService.php b/app/service/ScheduleService.php index 94685c4..69168bd 100644 --- a/app/service/ScheduleService.php +++ b/app/service/ScheduleService.php @@ -1,118 +1,118 @@ -where('nexttime', '>', 0)->where('nexttime', '<=', time())->where('active', 1)->select(); - if (count($list) == 0) { - return false; - } - echo '开始执行定时切换解析任务,共获取到' . count($list) . '个待执行任务' . "\n"; - foreach ($list as $row) { - try { - $this->execute_one($row); - echo '定时切换任务' . $row['id'] . '执行成功' . "\n"; - } catch (Exception $e) { - echo '定时切换任务' . $row['id'] . '执行失败,' . $e->getMessage() . "\n"; - } - } - config_set('schedule_time', date("Y-m-d H:i:s")); - return true; - } - - public function execute_one($row) - { - $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.ak,B.sk,B.ext')->find(); - if (!$drow) throw new Exception('域名不存在'); - - Db::name('sctask')->where('id', $row['id'])->update(['updatetime' => time()]); - - $domain = $row['rr'] . '.' . $drow['name']; - $dns = DnsHelper::getModel2($drow); - if ($row['switchtype'] == 1) { - $res = $dns->setDomainRecordStatus($row['recordid'], '1'); - if ($res) { - $this->add_log($domain, '启用解析', '定时启用解析成功'); - } else { - $this->add_log($domain, '启用解析失败', $dns->getError()); - } - } elseif ($row['switchtype'] == 2) { - $res = $dns->setDomainRecordStatus($row['recordid'], '0'); - if ($res) { - $this->add_log($domain, '暂停解析', '定时暂停解析成功'); - } else { - $this->add_log($domain, '暂停解析失败', $dns->getError()); - } - } elseif ($row['switchtype'] == 3) { - $res = $dns->deleteDomainRecord($row['recordid']); - if ($res) { - $this->add_log($domain, '删除解析', '定时删除解析成功'); - } else { - $this->add_log($domain, '删除解析失败', $dns->getError()); - } - } else { - $recordinfo = json_decode($row['recordinfo'], true); - if ($drow['type'] == 'cloudflare' && !isNullOrEmpty($row['line'])) { - $recordinfo['Line'] = $row['line']; - } - $res = $dns->updateDomainRecord($row['recordid'], $row['rr'], getDnsType($row['value']), $row['value'], $recordinfo['Line'], $recordinfo['TTL']); - if ($res) { - $this->add_log($domain, '修改解析', $row['rr'].' ['.getDnsType($row['value']).'] '.$row['value'].' (线路:'.$recordinfo['Line'].' TTL:'.$recordinfo['TTL'].')'); - } else { - $this->add_log($domain, '修改解析失败', $dns->getError()); - } - } - - $this->update_nexttime($row); - } - - public function update_nexttime($row) - { - if ($row['type'] == 1) { - if ($row['cycle'] == 2) { - $date = intval($row['switchdate']); - $nexttime = strtotime(date('Y-m-') . $date . ' ' . $row['switchtime'] . ':00'); - if ($nexttime <= time()) { - $nexttime = strtotime("+1 month", $nexttime); - } - } elseif ($row['cycle'] == 1) { - $weekday = intval($row['switchdate']); // 0-6, 0=周日 - $nexttime = strtotime("last Sunday +{$weekday} days {$row['switchtime']}:00"); - if ($nexttime <= time()) { - $nexttime = strtotime("+1 week", $nexttime); - if ($nexttime <= time()) { - $nexttime = strtotime("+1 week", $nexttime); - } - } - } else { - $nexttime = strtotime(date('Y-m-d') . ' ' . $row['switchtime'] . ':00'); - if ($nexttime <= time()) { - $nexttime = strtotime("+1 day", $nexttime); - } - } - } else { - $nexttime = strtotime($row['switchtime'] . ':00'); - if ($nexttime <= time()) { - $nexttime = 0; - } - } - Db::name('sctask')->where('id', $row['id'])->update(['nexttime' => $nexttime]); - } - - private function add_log($domain, $action, $data) - { - if (strlen($data) > 500) $data = substr($data, 0, 500); - Db::name('log')->insert(['uid' => 0, 'domain' => $domain, 'action' => $action, 'data' => $data, 'addtime' => date("Y-m-d H:i:s")]); - } -} +where('nexttime', '>', 0)->where('nexttime', '<=', time())->where('active', 1)->select(); + if (count($list) == 0) { + return false; + } + echo '开始执行定时切换解析任务,共获取到' . count($list) . '个待执行任务' . "\n"; + foreach ($list as $row) { + try { + $this->execute_one($row); + echo '定时切换任务' . $row['id'] . '执行成功' . "\n"; + } catch (Exception $e) { + echo '定时切换任务' . $row['id'] . '执行失败,' . $e->getMessage() . "\n"; + } + } + config_set('schedule_time', date("Y-m-d H:i:s")); + return true; + } + + public function execute_one($row) + { + $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.ak,B.sk,B.ext')->find(); + if (!$drow) throw new Exception('域名不存在'); + + Db::name('sctask')->where('id', $row['id'])->update(['updatetime' => time()]); + + $domain = $row['rr'] . '.' . $drow['name']; + $dns = DnsHelper::getModel2($drow); + if ($row['switchtype'] == 1) { + $res = $dns->setDomainRecordStatus($row['recordid'], '1'); + if ($res) { + $this->add_log($domain, '启用解析', '定时启用解析成功'); + } else { + $this->add_log($domain, '启用解析失败', $dns->getError()); + } + } elseif ($row['switchtype'] == 2) { + $res = $dns->setDomainRecordStatus($row['recordid'], '0'); + if ($res) { + $this->add_log($domain, '暂停解析', '定时暂停解析成功'); + } else { + $this->add_log($domain, '暂停解析失败', $dns->getError()); + } + } elseif ($row['switchtype'] == 3) { + $res = $dns->deleteDomainRecord($row['recordid']); + if ($res) { + $this->add_log($domain, '删除解析', '定时删除解析成功'); + } else { + $this->add_log($domain, '删除解析失败', $dns->getError()); + } + } else { + $recordinfo = json_decode($row['recordinfo'], true); + if ($drow['type'] == 'cloudflare' && !isNullOrEmpty($row['line'])) { + $recordinfo['Line'] = $row['line']; + } + $res = $dns->updateDomainRecord($row['recordid'], $row['rr'], getDnsType($row['value']), $row['value'], $recordinfo['Line'], $recordinfo['TTL']); + if ($res) { + $this->add_log($domain, '修改解析', $row['rr'].' ['.getDnsType($row['value']).'] '.$row['value'].' (线路:'.$recordinfo['Line'].' TTL:'.$recordinfo['TTL'].')'); + } else { + $this->add_log($domain, '修改解析失败', $dns->getError()); + } + } + + $this->update_nexttime($row); + } + + public function update_nexttime($row) + { + if ($row['type'] == 1) { + if ($row['cycle'] == 2) { + $date = intval($row['switchdate']); + $nexttime = strtotime(date('Y-m-') . $date . ' ' . $row['switchtime'] . ':00'); + if ($nexttime <= time()) { + $nexttime = strtotime("+1 month", $nexttime); + } + } elseif ($row['cycle'] == 1) { + $weekday = intval($row['switchdate']); // 0-6, 0=周日 + $nexttime = strtotime("last Sunday +{$weekday} days {$row['switchtime']}:00"); + if ($nexttime <= time()) { + $nexttime = strtotime("+1 week", $nexttime); + if ($nexttime <= time()) { + $nexttime = strtotime("+1 week", $nexttime); + } + } + } else { + $nexttime = strtotime(date('Y-m-d') . ' ' . $row['switchtime'] . ':00'); + if ($nexttime <= time()) { + $nexttime = strtotime("+1 day", $nexttime); + } + } + } else { + $nexttime = strtotime($row['switchtime'] . ':00'); + if ($nexttime <= time()) { + $nexttime = 0; + } + } + Db::name('sctask')->where('id', $row['id'])->update(['nexttime' => $nexttime]); + } + + private function add_log($domain, $action, $data) + { + if (strlen($data) > 500) $data = substr($data, 0, 500); + Db::name('log')->insert(['uid' => 0, 'domain' => $domain, 'action' => $action, 'data' => $data, 'addtime' => date("Y-m-d H:i:s")]); + } +} diff --git a/app/service/TaskRunner.php b/app/service/TaskRunner.php index b74b750..73ab2e3 100644 --- a/app/service/TaskRunner.php +++ b/app/service/TaskRunner.php @@ -1,135 +1,135 @@ -conn) { - $this->conn = NewDb::connect(); - } - return $this->conn; - } - - private function closeDb() - { - if ($this->conn) { - $this->conn->close(); - } - } - - public function execute($row) - { - if ($row['type'] == 3) { //条件开启解析 - $action = 0; - $remain = $this->db()->name('dmtask')->where(['did' => $row['did'], 'rr' => $row['rr'], 'type' => 1, 'status' => 0])->count(); - if ($remain <= $row['cycle'] && $row['status'] == 0) { - $action = 2; - $this->db()->name('dmtask')->where('id', $row['id'])->update(['status' => 1, 'errcount' => 0, 'switchtime' => time()]); - } elseif ($remain > $row['cycle'] && $row['status'] == 1) { - $action = 1; - $this->db()->name('dmtask')->where('id', $row['id'])->update(['status' => 0, 'errcount' => 0, 'switchtime' => time()]); - } - } else { - if ($row['checktype'] == 2) { - $result = CheckUtils::curl($row['checkurl'], $row['timeout'], $row['main_value'], $row['proxy'] == 1); - } elseif ($row['checktype'] == 1) { - $result = CheckUtils::tcp($row['main_value'], $row['checkurl'], $row['tcpport'], $row['timeout']); - } else { - $result = CheckUtils::ping($row['main_value'], $row['checkurl']); - } - - $action = 0; - if ($result['status'] && $row['status'] == 1) { - if ($row['cycle'] <= 1 || $row['errcount'] >= $row['cycle']) { - $this->db()->name('dmtask')->where('id', $row['id'])->update(['status' => 0, 'errcount' => 0, 'switchtime' => time()]); - $action = 2; - } else { - $this->db()->name('dmtask')->where('id', $row['id'])->inc('errcount')->update(); - } - } elseif (!$result['status'] && $row['status'] == 0) { - if ($row['cycle'] <= 1 || $row['errcount'] >= $row['cycle']) { - $this->db()->name('dmtask')->where('id', $row['id'])->update(['status' => 1, 'errcount' => 0, 'switchtime' => time()]); - $action = 1; - } else { - $this->db()->name('dmtask')->where('id', $row['id'])->inc('errcount')->update(); - } - } elseif ($row['errcount'] > 0) { - $this->db()->name('dmtask')->where('id', $row['id'])->update(['errcount' => 0]); - } - } - - 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(); - if (!$drow) { - echo '域名不存在(ID:'.$row['did'].')'."\n"; - $this->closeDb(); - return; - } - $row['domain'] = $row['rr'] . '.' . $drow['name']; - } - if ($action == 1) { - if ($row['type'] == 2) { - $dns = DnsHelper::getModel2($drow); - $recordinfo = json_decode($row['recordinfo'], true); - if ($drow['type'] == 'cloudflare' && $row['cdn'] == 1) { - $recordinfo['Line'] = '1'; - } - $res = $dns->updateDomainRecord($row['recordid'], $row['rr'], getDnsType($row['backup_value']), $row['backup_value'], $recordinfo['Line'], $recordinfo['TTL']); - if (!$res) { - $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '修改解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); - } - } elseif ($row['type'] == 1 || $row['type'] == 3) { - $dns = DnsHelper::getModel2($drow); - $res = $dns->setDomainRecordStatus($row['recordid'], '0'); - if (!$res) { - $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '暂停解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); - } - } - } elseif ($action == 2) { - if ($row['type'] == 2) { - $dns = DnsHelper::getModel2($drow); - $recordinfo = json_decode($row['recordinfo'], true); - if ($drow['type'] == 'cloudflare' && $row['cdn'] == 1) { - $recordinfo['Line'] = '0'; - } - $res = $dns->updateDomainRecord($row['recordid'], $row['rr'], getDnsType($row['main_value']), $row['main_value'], $recordinfo['Line'], $recordinfo['TTL']); - if (!$res) { - $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '修改解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); - } - } elseif ($row['type'] == 1 || $row['type'] == 3) { - $dns = DnsHelper::getModel2($drow); - $res = $dns->setDomainRecordStatus($row['recordid'], '1'); - if (!$res) { - $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '启用解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); - } - } - } else { - $this->closeDb(); - return; - } - - $this->db()->name('dmlog')->insert([ - 'taskid' => $row['id'], - 'action' => $action, - 'errmsg' => isset($result) ? ($result['status'] ? null : $result['errmsg']) : null, - 'date' => date('Y-m-d H:i:s') - ]); - $this->closeDb(); - - if ($row['type'] != 3) { - MsgNotice::send($action, $row, $result); - } - } -} +conn) { + $this->conn = NewDb::connect(); + } + return $this->conn; + } + + private function closeDb() + { + if ($this->conn) { + $this->conn->close(); + } + } + + public function execute($row) + { + if ($row['type'] == 3) { //条件开启解析 + $action = 0; + $remain = $this->db()->name('dmtask')->where(['did' => $row['did'], 'rr' => $row['rr'], 'type' => 1, 'status' => 0])->count(); + if ($remain <= $row['cycle'] && $row['status'] == 0) { + $action = 2; + $this->db()->name('dmtask')->where('id', $row['id'])->update(['status' => 1, 'errcount' => 0, 'switchtime' => time()]); + } elseif ($remain > $row['cycle'] && $row['status'] == 1) { + $action = 1; + $this->db()->name('dmtask')->where('id', $row['id'])->update(['status' => 0, 'errcount' => 0, 'switchtime' => time()]); + } + } else { + if ($row['checktype'] == 2) { + $result = CheckUtils::curl($row['checkurl'], $row['timeout'], $row['main_value'], $row['proxy'] == 1); + } elseif ($row['checktype'] == 1) { + $result = CheckUtils::tcp($row['main_value'], $row['checkurl'], $row['tcpport'], $row['timeout']); + } else { + $result = CheckUtils::ping($row['main_value'], $row['checkurl']); + } + + $action = 0; + if ($result['status'] && $row['status'] == 1) { + if ($row['cycle'] <= 1 || $row['errcount'] >= $row['cycle']) { + $this->db()->name('dmtask')->where('id', $row['id'])->update(['status' => 0, 'errcount' => 0, 'switchtime' => time()]); + $action = 2; + } else { + $this->db()->name('dmtask')->where('id', $row['id'])->inc('errcount')->update(); + } + } elseif (!$result['status'] && $row['status'] == 0) { + if ($row['cycle'] <= 1 || $row['errcount'] >= $row['cycle']) { + $this->db()->name('dmtask')->where('id', $row['id'])->update(['status' => 1, 'errcount' => 0, 'switchtime' => time()]); + $action = 1; + } else { + $this->db()->name('dmtask')->where('id', $row['id'])->inc('errcount')->update(); + } + } elseif ($row['errcount'] > 0) { + $this->db()->name('dmtask')->where('id', $row['id'])->update(['errcount' => 0]); + } + } + + 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(); + if (!$drow) { + echo '域名不存在(ID:'.$row['did'].')'."\n"; + $this->closeDb(); + return; + } + $row['domain'] = $row['rr'] . '.' . $drow['name']; + } + if ($action == 1) { + if ($row['type'] == 2) { + $dns = DnsHelper::getModel2($drow); + $recordinfo = json_decode($row['recordinfo'], true); + if ($drow['type'] == 'cloudflare' && $row['cdn'] == 1) { + $recordinfo['Line'] = '1'; + } + $res = $dns->updateDomainRecord($row['recordid'], $row['rr'], getDnsType($row['backup_value']), $row['backup_value'], $recordinfo['Line'], $recordinfo['TTL']); + if (!$res) { + $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '修改解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); + } + } elseif ($row['type'] == 1 || $row['type'] == 3) { + $dns = DnsHelper::getModel2($drow); + $res = $dns->setDomainRecordStatus($row['recordid'], '0'); + if (!$res) { + $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '暂停解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); + } + } + } elseif ($action == 2) { + if ($row['type'] == 2) { + $dns = DnsHelper::getModel2($drow); + $recordinfo = json_decode($row['recordinfo'], true); + if ($drow['type'] == 'cloudflare' && $row['cdn'] == 1) { + $recordinfo['Line'] = '0'; + } + $res = $dns->updateDomainRecord($row['recordid'], $row['rr'], getDnsType($row['main_value']), $row['main_value'], $recordinfo['Line'], $recordinfo['TTL']); + if (!$res) { + $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '修改解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); + } + } elseif ($row['type'] == 1 || $row['type'] == 3) { + $dns = DnsHelper::getModel2($drow); + $res = $dns->setDomainRecordStatus($row['recordid'], '1'); + if (!$res) { + $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '启用解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); + } + } + } else { + $this->closeDb(); + return; + } + + $this->db()->name('dmlog')->insert([ + 'taskid' => $row['id'], + 'action' => $action, + 'errmsg' => isset($result) ? ($result['status'] ? null : $result['errmsg']) : null, + 'date' => date('Y-m-d H:i:s') + ]); + $this->closeDb(); + + if ($row['type'] != 3) { + MsgNotice::send($action, $row, $result); + } + } +} diff --git a/app/sql/install.sql b/app/sql/install.sql index 5f25bd6..43462c7 100644 --- a/app/sql/install.sql +++ b/app/sql/install.sql @@ -1,256 +1,256 @@ -DROP TABLE IF EXISTS `dnsmgr_config`; -CREATE TABLE `dnsmgr_config` ( - `key` varchar(32) NOT NULL, - `value` TEXT DEFAULT NULL, - PRIMARY KEY (`key`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -INSERT INTO `dnsmgr_config` VALUES ('version', '1040'); -INSERT INTO `dnsmgr_config` VALUES ('notice_mail', '0'); -INSERT INTO `dnsmgr_config` VALUES ('notice_wxtpl', '0'); -INSERT INTO `dnsmgr_config` VALUES ('mail_smtp', 'smtp.qq.com'); -INSERT INTO `dnsmgr_config` VALUES ('mail_port', '465'); - -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', - `remark` varchar(100) DEFAULT NULL, - `addtime` datetime DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_domain`; -CREATE TABLE `dnsmgr_domain` ( - `id` int(11) unsigned NOT NULL auto_increment, - `aid` int(11) unsigned NOT NULL, - `name` varchar(255) NOT NULL, - `thirdid` varchar(60) DEFAULT NULL, - `addtime` datetime DEFAULT NULL, - `is_hide` tinyint(1) NOT NULL DEFAULT '0', - `is_sso` tinyint(1) NOT NULL DEFAULT '0', - `recordcount` int(1) NOT NULL DEFAULT '0', - `remark` varchar(100) DEFAULT NULL, - `is_notice` tinyint(1) NOT NULL DEFAULT '0', - `regtime` datetime DEFAULT NULL, - `expiretime` datetime DEFAULT NULL, - `checktime` datetime DEFAULT NULL, - `noticetime` datetime DEFAULT NULL, - `checkstatus` tinyint(1) NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_user`; -CREATE TABLE `dnsmgr_user` ( - `id` int(11) unsigned NOT NULL auto_increment, - `username` varchar(64) NOT NULL, - `password` varchar(80) NOT NULL, - `is_api` tinyint(1) NOT NULL DEFAULT '0', - `apikey` varchar(32) DEFAULT NULL, - `level` int(11) NOT NULL DEFAULT '0', - `regtime` datetime DEFAULT NULL, - `lasttime` datetime DEFAULT NULL, - `totp_open` tinyint(1) NOT NULL DEFAULT '0', - `totp_secret` varchar(100) DEFAULT NULL, - `status` tinyint(1) NOT NULL DEFAULT '1', - PRIMARY KEY (`id`), - KEY `username` (`username`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1000; - -DROP TABLE IF EXISTS `dnsmgr_permission`; -CREATE TABLE `dnsmgr_permission` ( - `id` int(11) unsigned NOT NULL auto_increment, - `uid` int(11) unsigned NOT NULL, - `domain` varchar(255) NOT NULL, - `sub` varchar(80) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `uid` (`uid`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_log`; -CREATE TABLE `dnsmgr_log` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `uid` int(11) unsigned NOT NULL, - `action` varchar(40) NOT NULL, - `domain` varchar(255) NOT NULL DEFAULT '', - `data` varchar(500) DEFAULT NULL, - `addtime` datetime NOT NULL, - PRIMARY KEY (`id`), - KEY `uid` (`uid`), - KEY `domain` (`domain`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_dmtask`; -CREATE TABLE `dnsmgr_dmtask` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `did` int(11) unsigned NOT NULL, - `rr` varchar(128) NOT NULL, - `recordid` varchar(60) NOT NULL, - `type` tinyint(1) NOT NULL DEFAULT 0, - `main_value` varchar(128) DEFAULT NULL, - `backup_value` varchar(128) DEFAULT NULL, - `checktype` tinyint(1) NOT NULL DEFAULT 0, - `checkurl` varchar(512) DEFAULT NULL, - `tcpport` int(5) DEFAULT NULL, - `frequency` tinyint(5) NOT NULL, - `cycle` tinyint(5) NOT NULL DEFAULT 3, - `timeout` tinyint(5) NOT NULL DEFAULT 2, - `remark` varchar(100) DEFAULT NULL, - `proxy` tinyint(1) NOT NULL DEFAULT 0, - `cdn` tinyint(1) NOT NULL DEFAULT 0, - `addtime` int(11) NOT NULL DEFAULT 0, - `checktime` int(11) NOT NULL DEFAULT 0, - `checknexttime` int(11) NOT NULL DEFAULT 0, - `switchtime` int(11) NOT NULL DEFAULT 0, - `errcount` tinyint(5) NOT NULL DEFAULT 0, - `status` tinyint(1) NOT NULL DEFAULT 0, - `active` tinyint(1) NOT NULL DEFAULT 0, - `recordinfo` varchar(200) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `did` (`did`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_dmlog`; -CREATE TABLE `dnsmgr_dmlog` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `taskid` int(11) unsigned NOT NULL, - `action` tinyint(4) NOT NULL DEFAULT 0, - `errmsg` varchar(100) DEFAULT NULL, - `date` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `taskid` (`taskid`), - KEY `date` (`date`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_optimizeip`; -CREATE TABLE `dnsmgr_optimizeip` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `did` int(11) unsigned NOT NULL, - `rr` varchar(128) NOT NULL, - `type` tinyint(1) NOT NULL DEFAULT 0, - `ip_type` varchar(10) NOT NULL, - `cdn_type` tinyint(5) NOT NULL DEFAULT 1, - `recordnum` tinyint(5) NOT NULL DEFAULT 2, - `ttl` int(5) NOT NULL DEFAULT 600, - `remark` varchar(100) DEFAULT NULL, - `addtime` datetime NOT NULL, - `updatetime` datetime DEFAULT NULL, - `status` tinyint(1) NOT NULL DEFAULT 0, - `active` tinyint(1) NOT NULL DEFAULT 0, - `errmsg` varchar(100) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `did` (`did`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_cert_account`; -CREATE TABLE `dnsmgr_cert_account` ( - `id` int(11) unsigned NOT NULL auto_increment, - `type` varchar(20) NOT NULL, - `name` varchar(255) NOT NULL, - `config` text DEFAULT NULL, - `ext` text DEFAULT NULL, - `remark` varchar(100) DEFAULT NULL, - `deploy` tinyint(1) NOT NULL DEFAULT '0', - `addtime` datetime DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_cert_order`; -CREATE TABLE `dnsmgr_cert_order` ( - `id` int(11) unsigned NOT NULL auto_increment, - `aid` int(11) unsigned NOT NULL, - `keytype` varchar(20) DEFAULT NULL, - `keysize` varchar(20) DEFAULT NULL, - `addtime` datetime DEFAULT NULL, - `updatetime` datetime DEFAULT NULL, - `processid` varchar(32) DEFAULT NULL, - `issuetime` datetime DEFAULT NULL, - `expiretime` datetime DEFAULT NULL, - `issuer` varchar(100) DEFAULT NULL, - `status` tinyint(1) NOT NULL DEFAULT '0', - `error` varchar(300) DEFAULT NULL, - `isauto` tinyint(1) NOT NULL DEFAULT '0', - `retry` tinyint(4) NOT NULL DEFAULT '0', - `retry2` tinyint(4) NOT NULL DEFAULT '0', - `retrytime` datetime DEFAULT NULL, - `islock` tinyint(1) NOT NULL DEFAULT '0', - `locktime` datetime DEFAULT NULL, - `issend` tinyint(1) NOT NULL DEFAULT '0', - `info` text DEFAULT NULL, - `dns` text DEFAULT NULL, - `fullchain` text DEFAULT NULL, - `privatekey` text DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_cert_domain`; -CREATE TABLE `dnsmgr_cert_domain` ( - `id` int(11) unsigned NOT NULL auto_increment, - `oid` int(11) unsigned NOT NULL, - `domain` varchar(255) NOT NULL, - `sort` int(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - KEY `oid` (`oid`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_cert_deploy`; -CREATE TABLE `dnsmgr_cert_deploy` ( - `id` int(11) unsigned NOT NULL auto_increment, - `aid` int(11) unsigned NOT NULL, - `oid` int(11) unsigned NOT NULL, - `issuetime` datetime DEFAULT NULL, - `config` text DEFAULT NULL, - `remark` varchar(100) DEFAULT NULL, - `addtime` datetime DEFAULT NULL, - `lasttime` datetime DEFAULT NULL, - `processid` varchar(32) DEFAULT NULL, - `status` tinyint(1) NOT NULL DEFAULT 0, - `error` varchar(300) DEFAULT NULL, - `active` tinyint(1) NOT NULL DEFAULT 0, - `retry` tinyint(4) NOT NULL DEFAULT '0', - `retrytime` datetime DEFAULT NULL, - `islock` tinyint(1) NOT NULL DEFAULT '0', - `locktime` datetime DEFAULT NULL, - `issend` tinyint(1) NOT NULL DEFAULT '0', - `info` text DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_cert_cname`; -CREATE TABLE `dnsmgr_cert_cname` ( - `id` int(11) unsigned NOT NULL auto_increment, - `domain` varchar(255) NOT NULL, - `did` int(11) unsigned NOT NULL, - `rr` varchar(128) NOT NULL, - `addtime` datetime DEFAULT NULL, - `status` tinyint(1) NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -DROP TABLE IF EXISTS `dnsmgr_sctask`; -CREATE TABLE `dnsmgr_sctask` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `did` int(11) unsigned NOT NULL, - `rr` varchar(128) NOT NULL, - `recordid` varchar(60) NOT NULL, - `type` tinyint(1) NOT NULL DEFAULT 0, - `cycle` tinyint(1) NOT NULL DEFAULT 0, - `switchtype` tinyint(1) NOT NULL DEFAULT 0, - `switchdate` varchar(10) DEFAULT NULL, - `switchtime` varchar(20) DEFAULT NULL, - `value` varchar(128) DEFAULT NULL, - `line` varchar(20) DEFAULT NULL, - `addtime` int(11) NOT NULL DEFAULT 0, - `updatetime` int(11) NOT NULL DEFAULT 0, - `nexttime` int(11) NOT NULL DEFAULT 0, - `active` tinyint(1) NOT NULL DEFAULT 0, - `recordinfo` varchar(200) DEFAULT NULL, - `remark` varchar(100) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `did` (`did`) +DROP TABLE IF EXISTS `dnsmgr_config`; +CREATE TABLE `dnsmgr_config` ( + `key` varchar(32) NOT NULL, + `value` TEXT DEFAULT NULL, + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO `dnsmgr_config` VALUES ('version', '1040'); +INSERT INTO `dnsmgr_config` VALUES ('notice_mail', '0'); +INSERT INTO `dnsmgr_config` VALUES ('notice_wxtpl', '0'); +INSERT INTO `dnsmgr_config` VALUES ('mail_smtp', 'smtp.qq.com'); +INSERT INTO `dnsmgr_config` VALUES ('mail_port', '465'); + +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', + `remark` varchar(100) DEFAULT NULL, + `addtime` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_domain`; +CREATE TABLE `dnsmgr_domain` ( + `id` int(11) unsigned NOT NULL auto_increment, + `aid` int(11) unsigned NOT NULL, + `name` varchar(255) NOT NULL, + `thirdid` varchar(60) DEFAULT NULL, + `addtime` datetime DEFAULT NULL, + `is_hide` tinyint(1) NOT NULL DEFAULT '0', + `is_sso` tinyint(1) NOT NULL DEFAULT '0', + `recordcount` int(1) NOT NULL DEFAULT '0', + `remark` varchar(100) DEFAULT NULL, + `is_notice` tinyint(1) NOT NULL DEFAULT '0', + `regtime` datetime DEFAULT NULL, + `expiretime` datetime DEFAULT NULL, + `checktime` datetime DEFAULT NULL, + `noticetime` datetime DEFAULT NULL, + `checkstatus` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_user`; +CREATE TABLE `dnsmgr_user` ( + `id` int(11) unsigned NOT NULL auto_increment, + `username` varchar(64) NOT NULL, + `password` varchar(80) NOT NULL, + `is_api` tinyint(1) NOT NULL DEFAULT '0', + `apikey` varchar(32) DEFAULT NULL, + `level` int(11) NOT NULL DEFAULT '0', + `regtime` datetime DEFAULT NULL, + `lasttime` datetime DEFAULT NULL, + `totp_open` tinyint(1) NOT NULL DEFAULT '0', + `totp_secret` varchar(100) DEFAULT NULL, + `status` tinyint(1) NOT NULL DEFAULT '1', + PRIMARY KEY (`id`), + KEY `username` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1000; + +DROP TABLE IF EXISTS `dnsmgr_permission`; +CREATE TABLE `dnsmgr_permission` ( + `id` int(11) unsigned NOT NULL auto_increment, + `uid` int(11) unsigned NOT NULL, + `domain` varchar(255) NOT NULL, + `sub` varchar(80) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `uid` (`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_log`; +CREATE TABLE `dnsmgr_log` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `uid` int(11) unsigned NOT NULL, + `action` varchar(40) NOT NULL, + `domain` varchar(255) NOT NULL DEFAULT '', + `data` varchar(500) DEFAULT NULL, + `addtime` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `uid` (`uid`), + KEY `domain` (`domain`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_dmtask`; +CREATE TABLE `dnsmgr_dmtask` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `did` int(11) unsigned NOT NULL, + `rr` varchar(128) NOT NULL, + `recordid` varchar(60) NOT NULL, + `type` tinyint(1) NOT NULL DEFAULT 0, + `main_value` varchar(128) DEFAULT NULL, + `backup_value` varchar(128) DEFAULT NULL, + `checktype` tinyint(1) NOT NULL DEFAULT 0, + `checkurl` varchar(512) DEFAULT NULL, + `tcpport` int(5) DEFAULT NULL, + `frequency` tinyint(5) NOT NULL, + `cycle` tinyint(5) NOT NULL DEFAULT 3, + `timeout` tinyint(5) NOT NULL DEFAULT 2, + `remark` varchar(100) DEFAULT NULL, + `proxy` tinyint(1) NOT NULL DEFAULT 0, + `cdn` tinyint(1) NOT NULL DEFAULT 0, + `addtime` int(11) NOT NULL DEFAULT 0, + `checktime` int(11) NOT NULL DEFAULT 0, + `checknexttime` int(11) NOT NULL DEFAULT 0, + `switchtime` int(11) NOT NULL DEFAULT 0, + `errcount` tinyint(5) NOT NULL DEFAULT 0, + `status` tinyint(1) NOT NULL DEFAULT 0, + `active` tinyint(1) NOT NULL DEFAULT 0, + `recordinfo` varchar(200) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `did` (`did`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_dmlog`; +CREATE TABLE `dnsmgr_dmlog` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `taskid` int(11) unsigned NOT NULL, + `action` tinyint(4) NOT NULL DEFAULT 0, + `errmsg` varchar(100) DEFAULT NULL, + `date` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `taskid` (`taskid`), + KEY `date` (`date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_optimizeip`; +CREATE TABLE `dnsmgr_optimizeip` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `did` int(11) unsigned NOT NULL, + `rr` varchar(128) NOT NULL, + `type` tinyint(1) NOT NULL DEFAULT 0, + `ip_type` varchar(10) NOT NULL, + `cdn_type` tinyint(5) NOT NULL DEFAULT 1, + `recordnum` tinyint(5) NOT NULL DEFAULT 2, + `ttl` int(5) NOT NULL DEFAULT 600, + `remark` varchar(100) DEFAULT NULL, + `addtime` datetime NOT NULL, + `updatetime` datetime DEFAULT NULL, + `status` tinyint(1) NOT NULL DEFAULT 0, + `active` tinyint(1) NOT NULL DEFAULT 0, + `errmsg` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `did` (`did`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_cert_account`; +CREATE TABLE `dnsmgr_cert_account` ( + `id` int(11) unsigned NOT NULL auto_increment, + `type` varchar(20) NOT NULL, + `name` varchar(255) NOT NULL, + `config` text DEFAULT NULL, + `ext` text DEFAULT NULL, + `remark` varchar(100) DEFAULT NULL, + `deploy` tinyint(1) NOT NULL DEFAULT '0', + `addtime` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_cert_order`; +CREATE TABLE `dnsmgr_cert_order` ( + `id` int(11) unsigned NOT NULL auto_increment, + `aid` int(11) unsigned NOT NULL, + `keytype` varchar(20) DEFAULT NULL, + `keysize` varchar(20) DEFAULT NULL, + `addtime` datetime DEFAULT NULL, + `updatetime` datetime DEFAULT NULL, + `processid` varchar(32) DEFAULT NULL, + `issuetime` datetime DEFAULT NULL, + `expiretime` datetime DEFAULT NULL, + `issuer` varchar(100) DEFAULT NULL, + `status` tinyint(1) NOT NULL DEFAULT '0', + `error` varchar(300) DEFAULT NULL, + `isauto` tinyint(1) NOT NULL DEFAULT '0', + `retry` tinyint(4) NOT NULL DEFAULT '0', + `retry2` tinyint(4) NOT NULL DEFAULT '0', + `retrytime` datetime DEFAULT NULL, + `islock` tinyint(1) NOT NULL DEFAULT '0', + `locktime` datetime DEFAULT NULL, + `issend` tinyint(1) NOT NULL DEFAULT '0', + `info` text DEFAULT NULL, + `dns` text DEFAULT NULL, + `fullchain` text DEFAULT NULL, + `privatekey` text DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_cert_domain`; +CREATE TABLE `dnsmgr_cert_domain` ( + `id` int(11) unsigned NOT NULL auto_increment, + `oid` int(11) unsigned NOT NULL, + `domain` varchar(255) NOT NULL, + `sort` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `oid` (`oid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_cert_deploy`; +CREATE TABLE `dnsmgr_cert_deploy` ( + `id` int(11) unsigned NOT NULL auto_increment, + `aid` int(11) unsigned NOT NULL, + `oid` int(11) unsigned NOT NULL, + `issuetime` datetime DEFAULT NULL, + `config` text DEFAULT NULL, + `remark` varchar(100) DEFAULT NULL, + `addtime` datetime DEFAULT NULL, + `lasttime` datetime DEFAULT NULL, + `processid` varchar(32) DEFAULT NULL, + `status` tinyint(1) NOT NULL DEFAULT 0, + `error` varchar(300) DEFAULT NULL, + `active` tinyint(1) NOT NULL DEFAULT 0, + `retry` tinyint(4) NOT NULL DEFAULT '0', + `retrytime` datetime DEFAULT NULL, + `islock` tinyint(1) NOT NULL DEFAULT '0', + `locktime` datetime DEFAULT NULL, + `issend` tinyint(1) NOT NULL DEFAULT '0', + `info` text DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_cert_cname`; +CREATE TABLE `dnsmgr_cert_cname` ( + `id` int(11) unsigned NOT NULL auto_increment, + `domain` varchar(255) NOT NULL, + `did` int(11) unsigned NOT NULL, + `rr` varchar(128) NOT NULL, + `addtime` datetime DEFAULT NULL, + `status` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP TABLE IF EXISTS `dnsmgr_sctask`; +CREATE TABLE `dnsmgr_sctask` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `did` int(11) unsigned NOT NULL, + `rr` varchar(128) NOT NULL, + `recordid` varchar(60) NOT NULL, + `type` tinyint(1) NOT NULL DEFAULT 0, + `cycle` tinyint(1) NOT NULL DEFAULT 0, + `switchtype` tinyint(1) NOT NULL DEFAULT 0, + `switchdate` varchar(10) DEFAULT NULL, + `switchtime` varchar(20) DEFAULT NULL, + `value` varchar(128) DEFAULT NULL, + `line` varchar(20) DEFAULT NULL, + `addtime` int(11) NOT NULL DEFAULT 0, + `updatetime` int(11) NOT NULL DEFAULT 0, + `nexttime` int(11) NOT NULL DEFAULT 0, + `active` tinyint(1) NOT NULL DEFAULT 0, + `recordinfo` varchar(200) DEFAULT NULL, + `remark` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `did` (`did`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/sql/update.sql b/app/sql/update.sql index 7caab52..42420e5 100644 --- a/app/sql/update.sql +++ b/app/sql/update.sql @@ -1,188 +1,188 @@ -CREATE TABLE IF NOT EXISTS `dnsmgr_config` ( - `key` varchar(32) NOT NULL, - `value` TEXT DEFAULT NULL, - PRIMARY KEY (`key`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `dnsmgr_dmtask` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `did` int(11) unsigned NOT NULL, - `rr` varchar(128) NOT NULL, - `recordid` varchar(60) NOT NULL, - `type` tinyint(1) NOT NULL DEFAULT 0, - `main_value` varchar(128) DEFAULT NULL, - `backup_value` varchar(128) DEFAULT NULL, - `checktype` tinyint(1) NOT NULL DEFAULT 0, - `checkurl` varchar(512) DEFAULT NULL, - `tcpport` int(5) DEFAULT NULL, - `frequency` tinyint(5) NOT NULL, - `cycle` tinyint(5) NOT NULL DEFAULT 3, - `timeout` tinyint(5) NOT NULL DEFAULT 2, - `remark` varchar(100) DEFAULT NULL, - `addtime` int(11) NOT NULL DEFAULT 0, - `checktime` int(11) NOT NULL DEFAULT 0, - `checknexttime` int(11) NOT NULL DEFAULT 0, - `switchtime` int(11) NOT NULL DEFAULT 0, - `errcount` tinyint(5) NOT NULL DEFAULT 0, - `status` tinyint(1) NOT NULL DEFAULT 0, - `active` tinyint(1) NOT NULL DEFAULT 0, - `recordinfo` varchar(200) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `did` (`did`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `dnsmgr_dmlog` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `taskid` int(11) unsigned NOT NULL, - `action` tinyint(4) NOT NULL DEFAULT 0, - `errmsg` varchar(100) DEFAULT NULL, - `date` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `taskid` (`taskid`), - KEY `date` (`date`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `dnsmgr_optimizeip` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `did` int(11) unsigned NOT NULL, - `rr` varchar(128) NOT NULL, - `type` tinyint(1) NOT NULL DEFAULT 0, - `ip_type` varchar(10) NOT NULL, - `cdn_type` tinyint(5) NOT NULL DEFAULT 1, - `recordnum` tinyint(5) NOT NULL DEFAULT 2, - `ttl` int(5) NOT NULL DEFAULT 600, - `remark` varchar(100) DEFAULT NULL, - `addtime` datetime NOT NULL, - `updatetime` datetime DEFAULT NULL, - `status` tinyint(1) NOT NULL DEFAULT 0, - `active` tinyint(1) NOT NULL DEFAULT 0, - `errmsg` varchar(100) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `did` (`did`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -ALTER TABLE `dnsmgr_domain` -ADD COLUMN `remark` varchar(100) DEFAULT NULL; - -ALTER TABLE `dnsmgr_dmtask` -ADD COLUMN `proxy` tinyint(1) NOT NULL DEFAULT 0; - -ALTER TABLE `dnsmgr_user` -ADD COLUMN `totp_open` tinyint(1) NOT NULL DEFAULT '0', -ADD COLUMN `totp_secret` varchar(100) DEFAULT NULL; - -CREATE TABLE IF NOT EXISTS `dnsmgr_cert_account` ( - `id` int(11) unsigned NOT NULL auto_increment, - `type` varchar(20) NOT NULL, - `name` varchar(255) NOT NULL, - `config` text DEFAULT NULL, - `ext` text DEFAULT NULL, - `remark` varchar(100) DEFAULT NULL, - `deploy` tinyint(1) NOT NULL DEFAULT '0', - `addtime` datetime DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `dnsmgr_cert_order` ( - `id` int(11) unsigned NOT NULL auto_increment, - `aid` int(11) unsigned NOT NULL, - `keytype` varchar(20) DEFAULT NULL, - `keysize` varchar(20) DEFAULT NULL, - `addtime` datetime DEFAULT NULL, - `updatetime` datetime DEFAULT NULL, - `processid` varchar(32) DEFAULT NULL, - `issuetime` datetime DEFAULT NULL, - `expiretime` datetime DEFAULT NULL, - `issuer` varchar(100) DEFAULT NULL, - `status` tinyint(1) NOT NULL DEFAULT '0', - `error` varchar(300) DEFAULT NULL, - `isauto` tinyint(1) NOT NULL DEFAULT '0', - `retry` tinyint(4) NOT NULL DEFAULT '0', - `retry2` tinyint(4) NOT NULL DEFAULT '0', - `retrytime` datetime DEFAULT NULL, - `islock` tinyint(1) NOT NULL DEFAULT '0', - `locktime` datetime DEFAULT NULL, - `issend` tinyint(1) NOT NULL DEFAULT '0', - `info` text DEFAULT NULL, - `dns` text DEFAULT NULL, - `fullchain` text DEFAULT NULL, - `privatekey` text DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `dnsmgr_cert_domain` ( - `id` int(11) unsigned NOT NULL auto_increment, - `oid` int(11) unsigned NOT NULL, - `domain` varchar(255) NOT NULL, - `sort` int(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - KEY `oid` (`oid`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `dnsmgr_cert_deploy` ( - `id` int(11) unsigned NOT NULL auto_increment, - `aid` int(11) unsigned NOT NULL, - `oid` int(11) unsigned NOT NULL, - `issuetime` datetime DEFAULT NULL, - `config` text DEFAULT NULL, - `remark` varchar(100) DEFAULT NULL, - `addtime` datetime DEFAULT NULL, - `lasttime` datetime DEFAULT NULL, - `processid` varchar(32) DEFAULT NULL, - `status` tinyint(1) NOT NULL DEFAULT 0, - `error` varchar(300) DEFAULT NULL, - `active` tinyint(1) NOT NULL DEFAULT 0, - `retry` tinyint(4) NOT NULL DEFAULT '0', - `retrytime` datetime DEFAULT NULL, - `islock` tinyint(1) NOT NULL DEFAULT '0', - `locktime` datetime DEFAULT NULL, - `issend` tinyint(1) NOT NULL DEFAULT '0', - `info` text DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS `dnsmgr_cert_cname` ( - `id` int(11) unsigned NOT NULL auto_increment, - `domain` varchar(255) NOT NULL, - `did` int(11) unsigned NOT NULL, - `rr` varchar(128) NOT NULL, - `addtime` datetime DEFAULT NULL, - `status` tinyint(1) NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -ALTER TABLE `dnsmgr_account` -ADD COLUMN `proxy` tinyint(1) NOT NULL DEFAULT '0'; - -ALTER TABLE `dnsmgr_dmtask` -ADD COLUMN `cdn` tinyint(1) NOT NULL DEFAULT 0; - -ALTER TABLE `dnsmgr_domain` -ADD COLUMN `is_notice` tinyint(1) NOT NULL DEFAULT '0', -ADD COLUMN `regtime` datetime DEFAULT NULL, -ADD COLUMN `expiretime` datetime DEFAULT NULL, -ADD COLUMN `checktime` datetime DEFAULT NULL, -ADD COLUMN `noticetime` datetime DEFAULT NULL, -ADD COLUMN `checkstatus` tinyint(1) NOT NULL DEFAULT '0'; - -CREATE TABLE IF NOT EXISTS `dnsmgr_sctask` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `did` int(11) unsigned NOT NULL, - `rr` varchar(128) NOT NULL, - `recordid` varchar(60) NOT NULL, - `type` tinyint(1) NOT NULL DEFAULT 0, - `cycle` tinyint(1) NOT NULL DEFAULT 0, - `switchtype` tinyint(1) NOT NULL DEFAULT 0, - `switchdate` varchar(10) DEFAULT NULL, - `switchtime` varchar(20) DEFAULT NULL, - `value` varchar(128) DEFAULT NULL, - `line` varchar(20) DEFAULT NULL, - `addtime` int(11) NOT NULL DEFAULT 0, - `updatetime` int(11) NOT NULL DEFAULT 0, - `nexttime` int(11) NOT NULL DEFAULT 0, - `active` tinyint(1) NOT NULL DEFAULT 0, - `recordinfo` varchar(200) DEFAULT NULL, - `remark` varchar(100) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `did` (`did`) +CREATE TABLE IF NOT EXISTS `dnsmgr_config` ( + `key` varchar(32) NOT NULL, + `value` TEXT DEFAULT NULL, + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `dnsmgr_dmtask` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `did` int(11) unsigned NOT NULL, + `rr` varchar(128) NOT NULL, + `recordid` varchar(60) NOT NULL, + `type` tinyint(1) NOT NULL DEFAULT 0, + `main_value` varchar(128) DEFAULT NULL, + `backup_value` varchar(128) DEFAULT NULL, + `checktype` tinyint(1) NOT NULL DEFAULT 0, + `checkurl` varchar(512) DEFAULT NULL, + `tcpport` int(5) DEFAULT NULL, + `frequency` tinyint(5) NOT NULL, + `cycle` tinyint(5) NOT NULL DEFAULT 3, + `timeout` tinyint(5) NOT NULL DEFAULT 2, + `remark` varchar(100) DEFAULT NULL, + `addtime` int(11) NOT NULL DEFAULT 0, + `checktime` int(11) NOT NULL DEFAULT 0, + `checknexttime` int(11) NOT NULL DEFAULT 0, + `switchtime` int(11) NOT NULL DEFAULT 0, + `errcount` tinyint(5) NOT NULL DEFAULT 0, + `status` tinyint(1) NOT NULL DEFAULT 0, + `active` tinyint(1) NOT NULL DEFAULT 0, + `recordinfo` varchar(200) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `did` (`did`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `dnsmgr_dmlog` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `taskid` int(11) unsigned NOT NULL, + `action` tinyint(4) NOT NULL DEFAULT 0, + `errmsg` varchar(100) DEFAULT NULL, + `date` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `taskid` (`taskid`), + KEY `date` (`date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `dnsmgr_optimizeip` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `did` int(11) unsigned NOT NULL, + `rr` varchar(128) NOT NULL, + `type` tinyint(1) NOT NULL DEFAULT 0, + `ip_type` varchar(10) NOT NULL, + `cdn_type` tinyint(5) NOT NULL DEFAULT 1, + `recordnum` tinyint(5) NOT NULL DEFAULT 2, + `ttl` int(5) NOT NULL DEFAULT 600, + `remark` varchar(100) DEFAULT NULL, + `addtime` datetime NOT NULL, + `updatetime` datetime DEFAULT NULL, + `status` tinyint(1) NOT NULL DEFAULT 0, + `active` tinyint(1) NOT NULL DEFAULT 0, + `errmsg` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `did` (`did`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE `dnsmgr_domain` +ADD COLUMN `remark` varchar(100) DEFAULT NULL; + +ALTER TABLE `dnsmgr_dmtask` +ADD COLUMN `proxy` tinyint(1) NOT NULL DEFAULT 0; + +ALTER TABLE `dnsmgr_user` +ADD COLUMN `totp_open` tinyint(1) NOT NULL DEFAULT '0', +ADD COLUMN `totp_secret` varchar(100) DEFAULT NULL; + +CREATE TABLE IF NOT EXISTS `dnsmgr_cert_account` ( + `id` int(11) unsigned NOT NULL auto_increment, + `type` varchar(20) NOT NULL, + `name` varchar(255) NOT NULL, + `config` text DEFAULT NULL, + `ext` text DEFAULT NULL, + `remark` varchar(100) DEFAULT NULL, + `deploy` tinyint(1) NOT NULL DEFAULT '0', + `addtime` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `dnsmgr_cert_order` ( + `id` int(11) unsigned NOT NULL auto_increment, + `aid` int(11) unsigned NOT NULL, + `keytype` varchar(20) DEFAULT NULL, + `keysize` varchar(20) DEFAULT NULL, + `addtime` datetime DEFAULT NULL, + `updatetime` datetime DEFAULT NULL, + `processid` varchar(32) DEFAULT NULL, + `issuetime` datetime DEFAULT NULL, + `expiretime` datetime DEFAULT NULL, + `issuer` varchar(100) DEFAULT NULL, + `status` tinyint(1) NOT NULL DEFAULT '0', + `error` varchar(300) DEFAULT NULL, + `isauto` tinyint(1) NOT NULL DEFAULT '0', + `retry` tinyint(4) NOT NULL DEFAULT '0', + `retry2` tinyint(4) NOT NULL DEFAULT '0', + `retrytime` datetime DEFAULT NULL, + `islock` tinyint(1) NOT NULL DEFAULT '0', + `locktime` datetime DEFAULT NULL, + `issend` tinyint(1) NOT NULL DEFAULT '0', + `info` text DEFAULT NULL, + `dns` text DEFAULT NULL, + `fullchain` text DEFAULT NULL, + `privatekey` text DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `dnsmgr_cert_domain` ( + `id` int(11) unsigned NOT NULL auto_increment, + `oid` int(11) unsigned NOT NULL, + `domain` varchar(255) NOT NULL, + `sort` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `oid` (`oid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `dnsmgr_cert_deploy` ( + `id` int(11) unsigned NOT NULL auto_increment, + `aid` int(11) unsigned NOT NULL, + `oid` int(11) unsigned NOT NULL, + `issuetime` datetime DEFAULT NULL, + `config` text DEFAULT NULL, + `remark` varchar(100) DEFAULT NULL, + `addtime` datetime DEFAULT NULL, + `lasttime` datetime DEFAULT NULL, + `processid` varchar(32) DEFAULT NULL, + `status` tinyint(1) NOT NULL DEFAULT 0, + `error` varchar(300) DEFAULT NULL, + `active` tinyint(1) NOT NULL DEFAULT 0, + `retry` tinyint(4) NOT NULL DEFAULT '0', + `retrytime` datetime DEFAULT NULL, + `islock` tinyint(1) NOT NULL DEFAULT '0', + `locktime` datetime DEFAULT NULL, + `issend` tinyint(1) NOT NULL DEFAULT '0', + `info` text DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `dnsmgr_cert_cname` ( + `id` int(11) unsigned NOT NULL auto_increment, + `domain` varchar(255) NOT NULL, + `did` int(11) unsigned NOT NULL, + `rr` varchar(128) NOT NULL, + `addtime` datetime DEFAULT NULL, + `status` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE `dnsmgr_account` +ADD COLUMN `proxy` tinyint(1) NOT NULL DEFAULT '0'; + +ALTER TABLE `dnsmgr_dmtask` +ADD COLUMN `cdn` tinyint(1) NOT NULL DEFAULT 0; + +ALTER TABLE `dnsmgr_domain` +ADD COLUMN `is_notice` tinyint(1) NOT NULL DEFAULT '0', +ADD COLUMN `regtime` datetime DEFAULT NULL, +ADD COLUMN `expiretime` datetime DEFAULT NULL, +ADD COLUMN `checktime` datetime DEFAULT NULL, +ADD COLUMN `noticetime` datetime DEFAULT NULL, +ADD COLUMN `checkstatus` tinyint(1) NOT NULL DEFAULT '0'; + +CREATE TABLE IF NOT EXISTS `dnsmgr_sctask` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `did` int(11) unsigned NOT NULL, + `rr` varchar(128) NOT NULL, + `recordid` varchar(60) NOT NULL, + `type` tinyint(1) NOT NULL DEFAULT 0, + `cycle` tinyint(1) NOT NULL DEFAULT 0, + `switchtype` tinyint(1) NOT NULL DEFAULT 0, + `switchdate` varchar(10) DEFAULT NULL, + `switchtime` varchar(20) DEFAULT NULL, + `value` varchar(128) DEFAULT NULL, + `line` varchar(20) DEFAULT NULL, + `addtime` int(11) NOT NULL DEFAULT 0, + `updatetime` int(11) NOT NULL DEFAULT 0, + `nexttime` int(11) NOT NULL DEFAULT 0, + `active` tinyint(1) NOT NULL DEFAULT 0, + `recordinfo` varchar(200) DEFAULT NULL, + `remark` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `did` (`did`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/utils/CertDnsUtils.php b/app/utils/CertDnsUtils.php index 0c58ada..d0a19c6 100644 --- a/app/utils/CertDnsUtils.php +++ b/app/utils/CertDnsUtils.php @@ -1,182 +1,182 @@ - $list) { - $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.name', $mainDomain)->field('A.*,B.type')->find(); - if (!$drow && preg_match('/^xn--/', $mainDomain)) { - $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.name', idn_to_utf8($mainDomain))->field('A.*,B.type')->find(); - } - if (!$drow) { - if ($cname) { - foreach ($list as $key => $row) { - if ($row['name'] == '_acme-challenge') { - $domain = $mainDomain; - } else { - $domain = str_replace('_acme-challenge.', '', $row['name']) . '.' . $mainDomain; - } - $cname_row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.domain', $domain)->field('A.*,B.name cnamedomain')->find(); - if ($cname_row) { - $row['name'] = $cname_row['rr']; - $cnameDomainList[$cname_row['cnamedomain']][] = $row; - unset($list[$key]); - } else { - throw new Exception('域名'.$domain.'未在本系统添加'); - } - } - } else { - throw new Exception('域名'.$mainDomain.'未在本系统添加'); - } - } - if (empty($list)) continue; - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - usort($list, function ($a, $b) { - return strcmp($a['name'], $b['name']); - }); - if ($drow['type'] == 'huawei') { - $list = self::getHuaweiDnsRecords($list); - } - $records = []; - foreach ($list as $row) { - $domain = $row['name'] . '.' . $mainDomain; - if (!isset($records[$row['name']])) $records[$row['name']] = $dns->getSubDomainRecords($row['name'], 1, 100); - if (!$records[$row['name']]) throw new Exception('获取'.$domain.'记录列表失败,'.$dns->getError()); - - $filter_records = array_filter($records[$row['name']]['list'], function ($v) use ($row) { - if (is_array($v['Value'])) $v['Value'] = implode(',', $v['Value']); - return $v['Type'] == $row['type'] && ($v['Value'] == $row['value'] || rtrim($v['Value'], '.') == $row['value']); - }); - if (!empty($filter_records)) { - foreach ($filter_records as $recordid => $record) { - unset($records[$row['name']]['list'][$recordid]); - } - continue; - } - - $filter_records = array_filter($records[$row['name']]['list'], function ($v) use ($row) { - return $v['Type'] == $row['type']; - }); - if (!empty($filter_records)) { - foreach ($filter_records as $recordid => $record) { - $dns->deleteDomainRecord($record['RecordId']); - unset($records[$row['name']]['list'][$recordid]); - $log('Delete DNS Record: '.$domain.' '.$row['type']); - } - } - - $ttl = $drow['type'] == 'namesilo' ? 3600 : 600; - $res = $dns->addDomainRecord($row['name'], $row['type'], $row['value'], DnsHelper::$line_name[$drow['type']]['DEF'], $ttl); - if (!$res && $row['type'] != 'CAA') throw new Exception('添加'.$domain.'解析记录失败,' . $dns->getError()); - $log('Add DNS Record: '.$domain.' '.$row['type'].' '.$row['value']); - } - } - if (!empty($cnameDomainList)) { - self::addDns($cnameDomainList, $log); - } - } - - private static function getHuaweiDnsRecords($list) - { - //将name相同的TXT记录合并 - $txt_records = []; - foreach ($list as $key => $row) { - if ($row['type'] == 'TXT') { - $txt_records[$row['name']][] = $row['value']; - unset($list[$key]); - } - } - foreach ($txt_records as $name => $rows) { - $list[] = ['name' => $name, 'type' => 'TXT', 'value' => '"' . implode('","', $rows) . '"']; - } - return $list; - } - - public static function delDns($dnsList, callable $log, $cname = false) - { - $cnameDomainList = []; - foreach ($dnsList as $mainDomain => $list) { - $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.name', $mainDomain)->field('A.*,B.type')->find(); - if (!$drow && preg_match('/^xn--/', $mainDomain)) { - $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.name', idn_to_utf8($mainDomain))->field('A.*,B.type')->find(); - } - if (!$drow) { - if ($cname) { - foreach ($list as $key => $row) { - if ($row['name'] == '_acme-challenge') { - $domain = $mainDomain; - } else { - $domain = str_replace('_acme-challenge.', '', $row['name']) . '.' . $mainDomain; - } - $cname_row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.domain', $domain)->field('A.*,B.name cnamedomain')->find(); - if ($cname_row) { - $row['name'] = $cname_row['rr']; - $cnameDomainList[$cname_row['cnamedomain']][] = $row; - unset($list[$key]); - } else { - throw new Exception('域名'.$domain.'未在本系统添加'); - } - } - } else { - throw new Exception('域名'.$mainDomain.'未在本系统添加'); - } - } - if (empty($list)) continue; - $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); - usort($list, function ($a, $b) { - return strcmp($a['name'], $b['name']); - }); - if ($drow['type'] == 'huawei') { - $list = self::getHuaweiDnsRecords($list); - } - $records = []; - foreach ($list as $row) { - //if ($row['type'] == 'CAA') continue; - $domain = $row['name'] . '.' . $mainDomain; - if (!isset($records[$row['name']])) $records[$row['name']] = $dns->getSubDomainRecords($row['name'], 1, 100); - if (!$records[$row['name']]) throw new Exception('获取'.$domain.'记录列表失败,'.$dns->getError()); - - $filter_records = array_filter($records[$row['name']]['list'], function ($v) use ($row) { - if (is_array($v['Value'])) $v['Value'] = implode(',', $v['Value']); - return $v['Type'] == $row['type'] && ($v['Value'] == $row['value'] || rtrim($v['Value'], '.') == $row['value']); - }); - if (empty($filter_records)) continue; - - foreach ($filter_records as $record) { - $dns->deleteDomainRecord($record['RecordId']); - $log('Delete DNS Record: '.$domain.' '.$row['type'].' '.$row['value']); - } - } - } - if (!empty($cnameDomainList)) { - self::delDns($cnameDomainList, $log); - } - } - - public static function verifyDns($dnsList) - { - if (empty($dnsList)) return true; - foreach ($dnsList as $mainDomain => $list) { - foreach ($list as $row) { - if ($row['type'] == 'CAA') continue; - $domain = $row['name'] . '.' . $mainDomain; - $result = DnsQueryUtils::get_dns_records($domain, $row['type']); - if (!$result || !in_array($row['value'], $result) && !in_array(strtolower($row['value']), $result)) { - $result = DnsQueryUtils::query_dns_doh($domain, $row['type']); - if (!$result || !in_array($row['value'], $result) && !in_array(strtolower($row['value']), $result)) { - return false; - } - } - } - } - return true; - } -} + $list) { + $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.name', $mainDomain)->field('A.*,B.type')->find(); + if (!$drow && preg_match('/^xn--/', $mainDomain)) { + $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.name', idn_to_utf8($mainDomain))->field('A.*,B.type')->find(); + } + if (!$drow) { + if ($cname) { + foreach ($list as $key => $row) { + if ($row['name'] == '_acme-challenge') { + $domain = $mainDomain; + } else { + $domain = str_replace('_acme-challenge.', '', $row['name']) . '.' . $mainDomain; + } + $cname_row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.domain', $domain)->field('A.*,B.name cnamedomain')->find(); + if ($cname_row) { + $row['name'] = $cname_row['rr']; + $cnameDomainList[$cname_row['cnamedomain']][] = $row; + unset($list[$key]); + } else { + throw new Exception('域名'.$domain.'未在本系统添加'); + } + } + } else { + throw new Exception('域名'.$mainDomain.'未在本系统添加'); + } + } + if (empty($list)) continue; + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + usort($list, function ($a, $b) { + return strcmp($a['name'], $b['name']); + }); + if ($drow['type'] == 'huawei') { + $list = self::getHuaweiDnsRecords($list); + } + $records = []; + foreach ($list as $row) { + $domain = $row['name'] . '.' . $mainDomain; + if (!isset($records[$row['name']])) $records[$row['name']] = $dns->getSubDomainRecords($row['name'], 1, 100); + if (!$records[$row['name']]) throw new Exception('获取'.$domain.'记录列表失败,'.$dns->getError()); + + $filter_records = array_filter($records[$row['name']]['list'], function ($v) use ($row) { + if (is_array($v['Value'])) $v['Value'] = implode(',', $v['Value']); + return $v['Type'] == $row['type'] && ($v['Value'] == $row['value'] || rtrim($v['Value'], '.') == $row['value']); + }); + if (!empty($filter_records)) { + foreach ($filter_records as $recordid => $record) { + unset($records[$row['name']]['list'][$recordid]); + } + continue; + } + + $filter_records = array_filter($records[$row['name']]['list'], function ($v) use ($row) { + return $v['Type'] == $row['type']; + }); + if (!empty($filter_records)) { + foreach ($filter_records as $recordid => $record) { + $dns->deleteDomainRecord($record['RecordId']); + unset($records[$row['name']]['list'][$recordid]); + $log('Delete DNS Record: '.$domain.' '.$row['type']); + } + } + + $ttl = $drow['type'] == 'namesilo' ? 3600 : 600; + $res = $dns->addDomainRecord($row['name'], $row['type'], $row['value'], DnsHelper::$line_name[$drow['type']]['DEF'], $ttl); + if (!$res && $row['type'] != 'CAA') throw new Exception('添加'.$domain.'解析记录失败,' . $dns->getError()); + $log('Add DNS Record: '.$domain.' '.$row['type'].' '.$row['value']); + } + } + if (!empty($cnameDomainList)) { + self::addDns($cnameDomainList, $log); + } + } + + private static function getHuaweiDnsRecords($list) + { + //将name相同的TXT记录合并 + $txt_records = []; + foreach ($list as $key => $row) { + if ($row['type'] == 'TXT') { + $txt_records[$row['name']][] = $row['value']; + unset($list[$key]); + } + } + foreach ($txt_records as $name => $rows) { + $list[] = ['name' => $name, 'type' => 'TXT', 'value' => '"' . implode('","', $rows) . '"']; + } + return $list; + } + + public static function delDns($dnsList, callable $log, $cname = false) + { + $cnameDomainList = []; + foreach ($dnsList as $mainDomain => $list) { + $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.name', $mainDomain)->field('A.*,B.type')->find(); + if (!$drow && preg_match('/^xn--/', $mainDomain)) { + $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.name', idn_to_utf8($mainDomain))->field('A.*,B.type')->find(); + } + if (!$drow) { + if ($cname) { + foreach ($list as $key => $row) { + if ($row['name'] == '_acme-challenge') { + $domain = $mainDomain; + } else { + $domain = str_replace('_acme-challenge.', '', $row['name']) . '.' . $mainDomain; + } + $cname_row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.domain', $domain)->field('A.*,B.name cnamedomain')->find(); + if ($cname_row) { + $row['name'] = $cname_row['rr']; + $cnameDomainList[$cname_row['cnamedomain']][] = $row; + unset($list[$key]); + } else { + throw new Exception('域名'.$domain.'未在本系统添加'); + } + } + } else { + throw new Exception('域名'.$mainDomain.'未在本系统添加'); + } + } + if (empty($list)) continue; + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + usort($list, function ($a, $b) { + return strcmp($a['name'], $b['name']); + }); + if ($drow['type'] == 'huawei') { + $list = self::getHuaweiDnsRecords($list); + } + $records = []; + foreach ($list as $row) { + //if ($row['type'] == 'CAA') continue; + $domain = $row['name'] . '.' . $mainDomain; + if (!isset($records[$row['name']])) $records[$row['name']] = $dns->getSubDomainRecords($row['name'], 1, 100); + if (!$records[$row['name']]) throw new Exception('获取'.$domain.'记录列表失败,'.$dns->getError()); + + $filter_records = array_filter($records[$row['name']]['list'], function ($v) use ($row) { + if (is_array($v['Value'])) $v['Value'] = implode(',', $v['Value']); + return $v['Type'] == $row['type'] && ($v['Value'] == $row['value'] || rtrim($v['Value'], '.') == $row['value']); + }); + if (empty($filter_records)) continue; + + foreach ($filter_records as $record) { + $dns->deleteDomainRecord($record['RecordId']); + $log('Delete DNS Record: '.$domain.' '.$row['type'].' '.$row['value']); + } + } + } + if (!empty($cnameDomainList)) { + self::delDns($cnameDomainList, $log); + } + } + + public static function verifyDns($dnsList) + { + if (empty($dnsList)) return true; + foreach ($dnsList as $mainDomain => $list) { + foreach ($list as $row) { + if ($row['type'] == 'CAA') continue; + $domain = $row['name'] . '.' . $mainDomain; + $result = DnsQueryUtils::get_dns_records($domain, $row['type']); + if (!$result || !in_array($row['value'], $result) && !in_array(strtolower($row['value']), $result)) { + $result = DnsQueryUtils::query_dns_doh($domain, $row['type']); + if (!$result || !in_array($row['value'], $result) && !in_array(strtolower($row['value']), $result)) { + return false; + } + } + } + } + return true; + } +} diff --git a/app/utils/CheckUtils.php b/app/utils/CheckUtils.php index 44b94b5..4d1b705 100644 --- a/app/utils/CheckUtils.php +++ b/app/utils/CheckUtils.php @@ -1,149 +1,149 @@ - false, 'errmsg' => 'Invalid URL', 'usetime' => 0]; - } - if (str_starts_with($urlarr['host'], '[') && str_ends_with($urlarr['host'], ']')) { - $urlarr['host'] = substr($urlarr['host'], 1, -1); - } - if (!empty($ip) && !filter_var($urlarr['host'], FILTER_VALIDATE_IP)) { - if (!filter_var($ip, FILTER_VALIDATE_IP)) { - $ip = gethostbyname($ip); - } - if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP)) { - $port = $urlarr['port'] ?? ($urlarr['scheme'] == 'https' ? 443 : 80); - $resolve = $urlarr['host'] . ':' . $port . ':' . $ip; - } - } - - $options = [ - 'timeout' => $timeout, - 'connect_timeout' => $timeout, - 'verify' => false, - 'headers' => [ - 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' - ], - 'http_errors' => false // 不抛出异常 - ]; - // 处理解析 - if (!empty($resolve)) { - $options['curl'] = [ - CURLOPT_DNS_USE_GLOBAL_CACHE => false, - CURLOPT_RESOLVE => [$resolve] - ]; - } - // 处理代理 - if ($proxy) { - $proxy_server = config_get('proxy_server'); - $proxy_port = intval(config_get('proxy_port')); - $proxy_userpwd = config_get('proxy_user').':'.config_get('proxy_pwd'); - $proxy_type = config_get('proxy_type'); - - if (!empty($proxy_server) && !empty($proxy_port)) { - match ($proxy_type) { - 'https' => $proxy_string = 'https://', - 'sock4' => $proxy_string = 'socks4://', - 'sock5' => $proxy_string = 'socks5://', - 'sock5h' => $proxy_string = 'socks5h://', - default => $proxy_string = 'http://', - }; - - if ($proxy_userpwd != ':') { - $proxy_string .= $proxy_userpwd . '@'; - } - - $proxy_string .= $proxy_server . ':' . $proxy_port; - $options['proxy'] = $proxy_string; - } - } - - try { - $client = new Client(); - $response = $client->request('GET', $url, $options); - $httpcode = $response->getStatusCode(); - - if ($httpcode < 200 || $httpcode >= 400) { - $status = false; - $errmsg = 'http_code=' . $httpcode; - } - } catch (GuzzleException $e) { - $status = false; - $errmsg = guzzle_error($e); - } - - $usetime = round((microtime(true) - $start) * 1000); - return ['status' => $status, 'errmsg' => $errmsg, 'usetime' => $usetime]; - } - - public static function tcp($target, $ip, $port, $timeout) - { - if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP)) $target = $ip; - if (str_ends_with($target, '.')) $target = substr($target, 0, -1); - if (!filter_var($target, FILTER_VALIDATE_IP) && checkDomain($target)) { - $target = gethostbyname($target); - if (!$target) return ['status' => false, 'errmsg' => 'DNS resolve failed', 'usetime' => 0]; - } - if (filter_var($target, FILTER_VALIDATE_IP) && str_contains($target, ':')) { - $target = '['.$target.']'; - } - $starttime = getMillisecond(); - $fp = @fsockopen($target, $port, $errCode, $errStr, $timeout); - if ($fp) { - $status = true; - fclose($fp); - } else { - $status = false; - } - $endtime = getMillisecond(); - $usetime = $endtime - $starttime; - return ['status' => $status, 'errmsg' => $errStr, 'usetime' => $usetime]; - } - - public static function ping($target, $ip) - { - if (!function_exists('exec')) return ['status' => false, 'errmsg' => 'exec函数不可用', 'usetime' => 0]; - if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP)) $target = $ip; - if (str_ends_with($target, '.')) $target = substr($target, 0, -1); - if (!filter_var($target, FILTER_VALIDATE_IP) && checkDomain($target)) { - $target = gethostbyname($target); - if (!$target) return ['status' => false, 'errmsg' => 'DNS resolve failed', 'usetime' => 0]; - } - if (!filter_var($target, FILTER_VALIDATE_IP)) { - return ['status' => false, 'errmsg' => 'Invalid IP address', 'usetime' => 0]; - } - $timeout = 1; - if (str_contains($target, ':')) { - exec('ping -6 -c 1 -w '.$timeout.' '.$target, $output, $return_var); - } else { - exec('ping -c 1 -w '.$timeout.' '.$target, $output, $return_var); - } - if (!empty($output[1])) { - if (strpos($output[1], '毫秒') !== false) { - $usetime = getSubstr($output[1], '时间=', ' 毫秒'); - } else { - $usetime = getSubstr($output[1], 'time=', ' ms'); - } - } - $usetime = !empty($usetime) ? round(trim($usetime)) : 0; - $errmsg = null; - if ($return_var !== 0) { - $usetime = $usetime == 0 ? $timeout * 1000 : $usetime; - $errmsg = 'ping timeout'; - } - return ['status' => $return_var === 0, 'errmsg' => $errmsg, 'usetime' => $usetime]; - } -} + false, 'errmsg' => 'Invalid URL', 'usetime' => 0]; + } + if (str_starts_with($urlarr['host'], '[') && str_ends_with($urlarr['host'], ']')) { + $urlarr['host'] = substr($urlarr['host'], 1, -1); + } + if (!empty($ip) && !filter_var($urlarr['host'], FILTER_VALIDATE_IP)) { + if (!filter_var($ip, FILTER_VALIDATE_IP)) { + $ip = gethostbyname($ip); + } + if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP)) { + $port = $urlarr['port'] ?? ($urlarr['scheme'] == 'https' ? 443 : 80); + $resolve = $urlarr['host'] . ':' . $port . ':' . $ip; + } + } + + $options = [ + 'timeout' => $timeout, + 'connect_timeout' => $timeout, + 'verify' => false, + 'headers' => [ + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' + ], + 'http_errors' => false // 不抛出异常 + ]; + // 处理解析 + if (!empty($resolve)) { + $options['curl'] = [ + CURLOPT_DNS_USE_GLOBAL_CACHE => false, + CURLOPT_RESOLVE => [$resolve] + ]; + } + // 处理代理 + if ($proxy) { + $proxy_server = config_get('proxy_server'); + $proxy_port = intval(config_get('proxy_port')); + $proxy_userpwd = config_get('proxy_user').':'.config_get('proxy_pwd'); + $proxy_type = config_get('proxy_type'); + + if (!empty($proxy_server) && !empty($proxy_port)) { + match ($proxy_type) { + 'https' => $proxy_string = 'https://', + 'sock4' => $proxy_string = 'socks4://', + 'sock5' => $proxy_string = 'socks5://', + 'sock5h' => $proxy_string = 'socks5h://', + default => $proxy_string = 'http://', + }; + + if ($proxy_userpwd != ':') { + $proxy_string .= $proxy_userpwd . '@'; + } + + $proxy_string .= $proxy_server . ':' . $proxy_port; + $options['proxy'] = $proxy_string; + } + } + + try { + $client = new Client(); + $response = $client->request('GET', $url, $options); + $httpcode = $response->getStatusCode(); + + if ($httpcode < 200 || $httpcode >= 400) { + $status = false; + $errmsg = 'http_code=' . $httpcode; + } + } catch (GuzzleException $e) { + $status = false; + $errmsg = guzzle_error($e); + } + + $usetime = round((microtime(true) - $start) * 1000); + return ['status' => $status, 'errmsg' => $errmsg, 'usetime' => $usetime]; + } + + public static function tcp($target, $ip, $port, $timeout) + { + if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP)) $target = $ip; + if (str_ends_with($target, '.')) $target = substr($target, 0, -1); + if (!filter_var($target, FILTER_VALIDATE_IP) && checkDomain($target)) { + $target = gethostbyname($target); + if (!$target) return ['status' => false, 'errmsg' => 'DNS resolve failed', 'usetime' => 0]; + } + if (filter_var($target, FILTER_VALIDATE_IP) && str_contains($target, ':')) { + $target = '['.$target.']'; + } + $starttime = getMillisecond(); + $fp = @fsockopen($target, $port, $errCode, $errStr, $timeout); + if ($fp) { + $status = true; + fclose($fp); + } else { + $status = false; + } + $endtime = getMillisecond(); + $usetime = $endtime - $starttime; + return ['status' => $status, 'errmsg' => $errStr, 'usetime' => $usetime]; + } + + public static function ping($target, $ip) + { + if (!function_exists('exec')) return ['status' => false, 'errmsg' => 'exec函数不可用', 'usetime' => 0]; + if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP)) $target = $ip; + if (str_ends_with($target, '.')) $target = substr($target, 0, -1); + if (!filter_var($target, FILTER_VALIDATE_IP) && checkDomain($target)) { + $target = gethostbyname($target); + if (!$target) return ['status' => false, 'errmsg' => 'DNS resolve failed', 'usetime' => 0]; + } + if (!filter_var($target, FILTER_VALIDATE_IP)) { + return ['status' => false, 'errmsg' => 'Invalid IP address', 'usetime' => 0]; + } + $timeout = 1; + if (str_contains($target, ':')) { + exec('ping -6 -c 1 -w '.$timeout.' '.$target, $output, $return_var); + } else { + exec('ping -c 1 -w '.$timeout.' '.$target, $output, $return_var); + } + if (!empty($output[1])) { + if (strpos($output[1], '毫秒') !== false) { + $usetime = getSubstr($output[1], '时间=', ' 毫秒'); + } else { + $usetime = getSubstr($output[1], 'time=', ' ms'); + } + } + $usetime = !empty($usetime) ? round(trim($usetime)) : 0; + $errmsg = null; + if ($return_var !== 0) { + $usetime = $usetime == 0 ? $timeout * 1000 : $usetime; + $errmsg = 'ping timeout'; + } + return ['status' => $return_var === 0, 'errmsg' => $errmsg, 'usetime' => $usetime]; + } +} diff --git a/app/utils/DnsQueryUtils.php b/app/utils/DnsQueryUtils.php index 74f9fb9..61495d5 100644 --- a/app/utils/DnsQueryUtils.php +++ b/app/utils/DnsQueryUtils.php @@ -1,65 +1,65 @@ - DNS_A, 'AAAA' => DNS_AAAA, 'CNAME' => DNS_CNAME, 'MX' => DNS_MX, 'TXT' => DNS_TXT]; - if (!array_key_exists($type, $dns_type)) return false; - try{ - $list = dns_get_record($domain, $dns_type[$type]); - }catch(Exception $e){ - return false; - } - if (!$list || empty($list)) return false; - $result = []; - foreach ($list as $row) { - if ($row['type'] == 'A') { - $result[] = $row['ip']; - } elseif ($row['type'] == 'AAAA') { - $result[] = $row['ipv6']; - } elseif ($row['type'] == 'CNAME') { - $result[] = $row['target']; - } elseif ($row['type'] == 'MX') { - $result[] = $row['target']; - } elseif ($row['type'] == 'TXT') { - $result[] = $row['txt']; - } - } - return $result; - } - - public static function query_dns_doh($domain, $type) - { - $dns_type = ['A' => 1, 'AAAA' => 28, 'CNAME' => 5, 'MX' => 15, 'TXT' => 16, 'SOA' => 6, 'NS' => 2, 'PTR' => 12, 'SRV' => 33, 'CAA' => 257]; - if (!array_key_exists($type, $dns_type)) return false; - $id = array_rand(self::$doh_servers); - $url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type]; - $data = get_curl($url); - $arr = json_decode($data, true); - if (!$arr) { - unset(self::$doh_servers[$id]); - $id = array_rand(self::$doh_servers); - $url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type]; - $data = get_curl($url); - $arr = json_decode($data, true); - if (!$arr) return false; - } - $result = []; - if (isset($arr['Answer'])) { - foreach ($arr['Answer'] as $row) { - $value = $row['data']; - if ($row['type'] == 5) $value = trim($value, '.'); - if ($row['type'] == 16) $value = trim($value, '"'); - $result[] = $value; - } - } - return $result; - } -} + DNS_A, 'AAAA' => DNS_AAAA, 'CNAME' => DNS_CNAME, 'MX' => DNS_MX, 'TXT' => DNS_TXT]; + if (!array_key_exists($type, $dns_type)) return false; + try{ + $list = dns_get_record($domain, $dns_type[$type]); + }catch(Exception $e){ + return false; + } + if (!$list || empty($list)) return false; + $result = []; + foreach ($list as $row) { + if ($row['type'] == 'A') { + $result[] = $row['ip']; + } elseif ($row['type'] == 'AAAA') { + $result[] = $row['ipv6']; + } elseif ($row['type'] == 'CNAME') { + $result[] = $row['target']; + } elseif ($row['type'] == 'MX') { + $result[] = $row['target']; + } elseif ($row['type'] == 'TXT') { + $result[] = $row['txt']; + } + } + return $result; + } + + public static function query_dns_doh($domain, $type) + { + $dns_type = ['A' => 1, 'AAAA' => 28, 'CNAME' => 5, 'MX' => 15, 'TXT' => 16, 'SOA' => 6, 'NS' => 2, 'PTR' => 12, 'SRV' => 33, 'CAA' => 257]; + if (!array_key_exists($type, $dns_type)) return false; + $id = array_rand(self::$doh_servers); + $url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type]; + $data = get_curl($url); + $arr = json_decode($data, true); + if (!$arr) { + unset(self::$doh_servers[$id]); + $id = array_rand(self::$doh_servers); + $url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type]; + $data = get_curl($url); + $arr = json_decode($data, true); + if (!$arr) return false; + } + $result = []; + if (isset($arr['Answer'])) { + foreach ($arr['Answer'] as $row) { + $value = $row['data']; + if ($row['type'] == 5) $value = trim($value, '.'); + if ($row['type'] == 16) $value = trim($value, '"'); + $result[] = $value; + } + } + return $result; + } +} diff --git a/app/utils/MsgNotice.php b/app/utils/MsgNotice.php index 6c186ac..4870e4b 100644 --- a/app/utils/MsgNotice.php +++ b/app/utils/MsgNotice.php @@ -1,322 +1,322 @@ -您的域名 '.$task['domain'].''.$task['main_value'].' 记录发生了异常'; - if ($task['type'] == 2) { - $mail_content .= ',已自动切换为备用解析记录 '.$task['backup_value'].' '; - } elseif ($task['type'] == 1) { - $mail_content .= ',已自动暂停解析'; - } else { - $mail_content .= ',请及时处理'; - } - if (!empty($result['errmsg'])) { - $mail_content .= '。
异常信息:'.$result['errmsg'].''; - } - } else { - $mail_title = 'DNS容灾切换-恢复正常通知'; - $mail_content = '尊敬的用户,您好:
您的域名 '.$task['domain'].''.$task['main_value'].' 记录已恢复正常'; - if ($task['type'] == 2) { - $mail_content .= ',已自动切换回当前解析记录'; - } elseif ($task['type'] == 1) { - $mail_content .= ',已自动开启解析'; - } - $lasttime = convert_second(time() - $task['switchtime']); - $mail_content .= '。
异常持续时间:'.$lasttime; - } - if (!empty($task['remark'])) { - $mail_title .= '('.$task['remark'].')'; - } - if (!empty($task['remark'])) { - $mail_content .= '
备注:'.$task['remark']; - } - $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s').''; - - if (config_get('notice_mail') == 1) { - $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); - self::send_mail($mail_name, $mail_title, $mail_content); - } - if (config_get('notice_wxtpl') == 1) { - $content = str_replace(['
', '', ''], ["\n\n", '**', '**'], $mail_content); - self::send_wechat_tplmsg($mail_title, strip_tags($content)); - } - if (config_get('notice_tgbot') == 1) { - $content = str_replace('
', "\n", $mail_content); - $content = "".$mail_title."\n".strip_tags($content); - self::send_telegram_bot($content); - } - if (config_get('notice_webhook') == 1) { - $content = str_replace(['
', '', ''], ["\n", '**', '**'], $mail_content); - self::send_webhook($mail_title, $content); - } - } - - public static function cert_order_send($id, $result) - { - $row = Db::name('cert_order')->field('id,aid,issuetime,expiretime,issuer,status,error')->where('id', $id)->find(); - if (!$row) return; - $domainList = Db::name('cert_domain')->where('oid', $id)->column('domain'); - if (empty($domainList)) return; - if ($row['aid'] == 0) { - if (count($domainList) > 1) { - $mail_title = $domainList[0] . '等' . count($domainList) . '个域名SSL证书即将到期提醒'; - } else { - $mail_title = $domainList[0] . '域名SSL证书即将到期提醒'; - } - $mail_content = '尊敬的用户,您好:您有一张SSL证书将在'.config_get('cert_renewdays', 7).'天后到期,该证书为手动续期证书,请及时续期!
证书域名: '.implode('、', $domainList).'
签发时间: '.$row['issuetime'].'
到期时间: '.$row['expiretime'].'
颁发机构: '.$row['issuer']; - } else { - $type = Db::name('cert_account')->where('id', $row['aid'])->value('type'); - if ($result) { - if (count($domainList) > 1) { - $mail_title = $domainList[0] . '等' . count($domainList) . '个域名SSL证书签发成功通知'; - } else { - $mail_title = $domainList[0] . '域名SSL证书签发成功通知'; - } - $mail_content = '尊敬的用户,您好:您的SSL证书已签发成功!
证书账户: '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')
证书域名: '.implode('、', $domainList).'
签发时间: '.$row['issuetime'].'
到期时间: '.$row['expiretime'].'
颁发机构: '.$row['issuer']; - } else { - $status_arr = [0 => '失败', -1 => '购买证书失败', -2 => '创建订单失败', -3 => '添加DNS失败', -4 => '验证DNS失败', -5 => '验证订单失败', -6 => '订单验证未通过', -7 => '签发证书失败']; - if(count($domainList) > 1){ - $mail_title = $domainList[0].'等'.count($domainList).'个域名SSL证书'.$status_arr[$row['status']].'通知'; - }else{ - $mail_title = $domainList[0].'域名SSL证书'.$status_arr[$row['status']].'通知'; - } - $mail_content = '尊敬的用户,您好:您的SSL证书'.$status_arr[$row['status']].'!
证书账户: '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')
证书域名: '.implode('、', $domainList).'
失败时间: '.date('Y-m-d H:i:s').'
失败原因: '.$row['error'].''; - } - } - $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s').''; - - self::cert_send($mail_title, $mail_content, $result); - Db::name('cert_order')->where('id', $id)->update(['issend' => 1]); - } - - public static function cert_deploy_send($id, $result) - { - $row = Db::name('cert_deploy')->field('id,aid,oid,remark,status,error')->where('id', $id)->find(); - if (!$row) return; - $account = Db::name('cert_account')->field('id,type,name,remark')->where('id', $row['aid'])->find(); - $domainList = Db::name('cert_domain')->where('oid', $row['oid'])->column('domain'); - $typename = DeployHelper::$deploy_config[$account['type']]['name']; - $mail_title = $typename; - if(!empty($row['remark'])) $mail_title .= '('.$row['remark'].')'; - $mail_title .= 'SSL证书部署'.($result?'成功':'失败').'通知'; - if ($result) { - $mail_content = '尊敬的用户,您好:您的SSL证书已成功部署到'.$typename.'!
自动部署账户: ['.$account['id'].']'.$typename.'('.($account['remark']?$account['remark']:$account['name']).')
关联SSL证书: ['.$row['oid'].']'.implode('、', $domainList).'
任务备注: '.($row['remark']?$row['remark']:'无'); - } else { - $mail_content = '尊敬的用户,您好:您的SSL证书部署失败!
失败原因: '.$row['error'].'
自动部署账户: ['.$account['id'].']'.$typename.'('.($account['remark']?$account['remark']:$account['name']).')
关联SSL证书: ['.$row['oid'].']'.implode('、', $domainList).'
任务备注: '.($row['remark']?$row['remark']:'无'); - } - $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s').''; - - self::cert_send($mail_title, $mail_content, $result); - Db::name('cert_deploy')->where('id', $id)->update(['issend' => 1]); - } - - private static function cert_send($mail_title, $mail_content, $result) - { - if (config_get('cert_notice_mail') == 1 || config_get('cert_notice_mail') == 2 && !$result) { - $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); - self::send_mail($mail_name, $mail_title, $mail_content); - } - if (config_get('cert_notice_wxtpl') == 1 || config_get('cert_notice_wxtpl') == 2 && !$result) { - $content = str_replace(['
', '', ''], ["\n\n", '**', '**'], $mail_content); - self::send_wechat_tplmsg($mail_title, strip_tags($content)); - } - if (config_get('cert_notice_tgbot') == 1 || config_get('cert_notice_tgbot') == 2 && !$result) { - $content = str_replace('
', "\n", $mail_content); - $content = "".$mail_title."\n".strip_tags($content); - self::send_telegram_bot($content); - } - if (config_get('cert_notice_webhook') == 1) { - $content = str_replace(['*', '
', '', ''], ['\*', "\n", '**', '**'], $mail_content); - self::send_webhook($mail_title, $content); - } - } - - public static function expire_notice_send($day, $list) - { - $mail_title = '您有'.count($list).'个域名即将在'.$day.'天后到期'; - $mail_content = '尊敬的用户,您好:您有'.count($list).'个域名即将在'.$day.'天后到期!
域名&到期时间:
'; - foreach ($list as $domain) { - $mail_content .= ''.$domain['name'].' - '.$domain['expiretime'].'
'; - } - $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s').''; - - if (config_get('expire_notice_mail') == 1 || config_get('expire_notice_mail') == 2) { - $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); - self::send_mail($mail_name, $mail_title, $mail_content); - } - if (config_get('expire_notice_wxtpl') == 1 || config_get('expire_notice_wxtpl') == 2) { - $content = str_replace(['
', '', ''], ["\n\n", '**', '**'], $mail_content); - self::send_wechat_tplmsg($mail_title, strip_tags($content)); - } - if (config_get('expire_notice_tgbot') == 1 || config_get('expire_notice_tgbot') == 2) { - $content = str_replace('
', "\n", $mail_content); - $content = "".$mail_title."\n".strip_tags($content); - self::send_telegram_bot($content); - } - if (config_get('expire_notice_webhook') == 1) { - $content = str_replace(['*', '
', '', ''], ['\*', "\n", '**', '**'], $mail_content); - self::send_webhook($mail_title, $content); - } - } - - public static function send_mail($to, $sub, $msg) - { - $mail_type = config_get('mail_type'); - if ($mail_type == 1) { - $mail = new \app\lib\mail\Sendcloud(config_get('mail_apiuser'), config_get('mail_apikey')); - return $mail->send($to, $sub, $msg, config_get('mail_name'), self::$sitename); - } elseif ($mail_type == 2) { - $mail = new \app\lib\mail\Aliyun(config_get('mail_apiuser'), config_get('mail_apikey')); - return $mail->send($to, $sub, $msg, config_get('mail_name'), self::$sitename); - } else { - $mail_name = config_get('mail_name'); - $mail_port = intval(config_get('mail_port')); - $mail_smtp = config_get('mail_smtp'); - $mail_pwd = config_get('mail_pwd'); - if (!$mail_name || !$mail_port || !$mail_smtp || !$mail_pwd) return false; - $mail = new PHPMailer(true); - $mail->setLanguage('zh_cn'); - try { - $mail->SMTPDebug = 0; - $mail->CharSet = 'UTF-8'; - $mail->Timeout = 5; - $mail->isSMTP(); - $mail->Host = $mail_smtp; - $mail->SMTPAuth = true; - $mail->Username = $mail_name; - $mail->Password = $mail_pwd; - if ($mail_port == 587) $mail->SMTPSecure = 'tls'; - else if ($mail_port >= 465) $mail->SMTPSecure = 'ssl'; - else $mail->SMTPAutoTLS = false; - $mail->Port = $mail_port; - $mail->setFrom($mail_name, self::$sitename); - $mail->addAddress($to); - $mail->addReplyTo($mail_name, self::$sitename); - $mail->isHTML(true); - $mail->Subject = $sub; - $mail->Body = $msg; - $mail->send(); - return true; - } catch (\Exception $e) { - return $mail->ErrorInfo; - } - } - } - - public static function send_wechat_tplmsg($title, $content) - { - $wechat_apptoken = config_get('wechat_apptoken'); - $wechat_appuid = config_get('wechat_appuid'); - if (!$wechat_apptoken || !$wechat_appuid) return false; - $url = 'https://wxpusher.zjiecode.com/api/send/message'; - $post = ['appToken' => $wechat_apptoken, 'content' => $content, 'summary' => $title, 'contentType' => 3, 'uids' => [$wechat_appuid]]; - $result = get_curl($url, json_encode($post), 0, 0, 0, 0, ['Content-Type' => 'application/json; charset=UTF-8']); - $arr = json_decode($result, true); - if (isset($arr['success']) && $arr['success'] == true) { - return true; - } else { - return $arr['msg'] ?? '请求失败'; - } - } - - public static function send_telegram_bot($content) - { - $tgbot_token = config_get('tgbot_token'); - $tgbot_chatid = config_get('tgbot_chatid'); - if (!$tgbot_token || !$tgbot_chatid) return false; - $tgbot_url = 'https://api.telegram.org'; - if (config_get('tgbot_proxy') == 2) { - $tgbot_url_n = config_get('tgbot_url'); - if (!empty($tgbot_url_n)) { - $tgbot_url = rtrim($tgbot_url_n, '/'); - } - } - $url = $tgbot_url.'/bot'.$tgbot_token.'/sendMessage'; - $post = ['chat_id' => $tgbot_chatid, 'text' => $content, 'parse_mode' => 'HTML']; - $result = self::telegram_curl($url, http_build_query($post)); - $arr = json_decode($result, true); - if (isset($arr['ok']) && $arr['ok'] == true) { - return true; - } else { - return $arr['description'] ?? '请求失败'; - } - } - - public static function send_webhook($title, $content) - { - $url = config_get('webhook_url'); - if (!$url || !parse_url($url)) return false; - if (strpos($url, 'oapi.dingtalk.com')) { - $content = '### '.$title." \n ".str_replace("\n", " \n ", $content); - $post = [ - 'msgtype' => 'markdown', - 'markdown' => [ - 'title' => $title, - 'text' => $content, - ], - ]; - } elseif (strpos($url, 'qyapi.weixin.qq.com')) { - $content = '## '.$title."\n".$content; - $post = [ - 'msgtype' => 'markdown', - 'markdown' => [ - 'content' => $content, - ], - ]; - } elseif (strpos($url, 'open.feishu.cn') || strpos($url, 'open.larksuite.com')) { - $content = str_replace(['\*', '**'], ['*', ''], strip_tags($content)); - $post = [ - 'msg_type' => 'text', - 'content' => [ - 'text' => $content, - ], - ]; - } else { - return '不支持的Webhook地址'; - } - $result = get_curl($url, json_encode($post), 0, 0, 0, 0, ['Content-Type' => 'application/json; charset=UTF-8']); - $arr = json_decode($result, true); - if (isset($arr['errcode']) && $arr['errcode'] == 0 || isset($arr['code']) && $arr['code'] == 0) { - return true; - } else { - return $arr['errmsg'] ?? (isset($arr['msg']) ? $arr['msg'] : '请求失败'); - } - } - - private static function telegram_curl($url, $post) - { - $ch = curl_init(); - if (config_get('tgbot_proxy') == 1) { - curl_set_proxy($ch); - } - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - $httpheader[] = "Accept: */*"; - $httpheader[] = "Accept-Encoding: gzip,deflate,sdch"; - $httpheader[] = "Accept-Language: zh-CN,zh;q=0.8"; - $httpheader[] = "Connection: close"; - curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $post); - curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Linux; U; Android 4.0.4; es-mx; HTC_One_X Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0"); - curl_setopt($ch, CURLOPT_ENCODING, "gzip"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - $ret = curl_exec($ch); - curl_close($ch); - return $ret; - } -} +您的域名 '.$task['domain'].''.$task['main_value'].' 记录发生了异常'; + if ($task['type'] == 2) { + $mail_content .= ',已自动切换为备用解析记录 '.$task['backup_value'].' '; + } elseif ($task['type'] == 1) { + $mail_content .= ',已自动暂停解析'; + } else { + $mail_content .= ',请及时处理'; + } + if (!empty($result['errmsg'])) { + $mail_content .= '。
异常信息:'.$result['errmsg'].''; + } + } else { + $mail_title = 'DNS容灾切换-恢复正常通知'; + $mail_content = '尊敬的用户,您好:
您的域名 '.$task['domain'].''.$task['main_value'].' 记录已恢复正常'; + if ($task['type'] == 2) { + $mail_content .= ',已自动切换回当前解析记录'; + } elseif ($task['type'] == 1) { + $mail_content .= ',已自动开启解析'; + } + $lasttime = convert_second(time() - $task['switchtime']); + $mail_content .= '。
异常持续时间:'.$lasttime; + } + if (!empty($task['remark'])) { + $mail_title .= '('.$task['remark'].')'; + } + if (!empty($task['remark'])) { + $mail_content .= '
备注:'.$task['remark']; + } + $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s').''; + + if (config_get('notice_mail') == 1) { + $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); + self::send_mail($mail_name, $mail_title, $mail_content); + } + if (config_get('notice_wxtpl') == 1) { + $content = str_replace(['
', '', ''], ["\n\n", '**', '**'], $mail_content); + self::send_wechat_tplmsg($mail_title, strip_tags($content)); + } + if (config_get('notice_tgbot') == 1) { + $content = str_replace('
', "\n", $mail_content); + $content = "".$mail_title."\n".strip_tags($content); + self::send_telegram_bot($content); + } + if (config_get('notice_webhook') == 1) { + $content = str_replace(['
', '', ''], ["\n", '**', '**'], $mail_content); + self::send_webhook($mail_title, $content); + } + } + + public static function cert_order_send($id, $result) + { + $row = Db::name('cert_order')->field('id,aid,issuetime,expiretime,issuer,status,error')->where('id', $id)->find(); + if (!$row) return; + $domainList = Db::name('cert_domain')->where('oid', $id)->column('domain'); + if (empty($domainList)) return; + if ($row['aid'] == 0) { + if (count($domainList) > 1) { + $mail_title = $domainList[0] . '等' . count($domainList) . '个域名SSL证书即将到期提醒'; + } else { + $mail_title = $domainList[0] . '域名SSL证书即将到期提醒'; + } + $mail_content = '尊敬的用户,您好:您有一张SSL证书将在'.config_get('cert_renewdays', 7).'天后到期,该证书为手动续期证书,请及时续期!
证书域名: '.implode('、', $domainList).'
签发时间: '.$row['issuetime'].'
到期时间: '.$row['expiretime'].'
颁发机构: '.$row['issuer']; + } else { + $type = Db::name('cert_account')->where('id', $row['aid'])->value('type'); + if ($result) { + if (count($domainList) > 1) { + $mail_title = $domainList[0] . '等' . count($domainList) . '个域名SSL证书签发成功通知'; + } else { + $mail_title = $domainList[0] . '域名SSL证书签发成功通知'; + } + $mail_content = '尊敬的用户,您好:您的SSL证书已签发成功!
证书账户: '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')
证书域名: '.implode('、', $domainList).'
签发时间: '.$row['issuetime'].'
到期时间: '.$row['expiretime'].'
颁发机构: '.$row['issuer']; + } else { + $status_arr = [0 => '失败', -1 => '购买证书失败', -2 => '创建订单失败', -3 => '添加DNS失败', -4 => '验证DNS失败', -5 => '验证订单失败', -6 => '订单验证未通过', -7 => '签发证书失败']; + if(count($domainList) > 1){ + $mail_title = $domainList[0].'等'.count($domainList).'个域名SSL证书'.$status_arr[$row['status']].'通知'; + }else{ + $mail_title = $domainList[0].'域名SSL证书'.$status_arr[$row['status']].'通知'; + } + $mail_content = '尊敬的用户,您好:您的SSL证书'.$status_arr[$row['status']].'!
证书账户: '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')
证书域名: '.implode('、', $domainList).'
失败时间: '.date('Y-m-d H:i:s').'
失败原因: '.$row['error'].''; + } + } + $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s').''; + + self::cert_send($mail_title, $mail_content, $result); + Db::name('cert_order')->where('id', $id)->update(['issend' => 1]); + } + + public static function cert_deploy_send($id, $result) + { + $row = Db::name('cert_deploy')->field('id,aid,oid,remark,status,error')->where('id', $id)->find(); + if (!$row) return; + $account = Db::name('cert_account')->field('id,type,name,remark')->where('id', $row['aid'])->find(); + $domainList = Db::name('cert_domain')->where('oid', $row['oid'])->column('domain'); + $typename = DeployHelper::$deploy_config[$account['type']]['name']; + $mail_title = $typename; + if(!empty($row['remark'])) $mail_title .= '('.$row['remark'].')'; + $mail_title .= 'SSL证书部署'.($result?'成功':'失败').'通知'; + if ($result) { + $mail_content = '尊敬的用户,您好:您的SSL证书已成功部署到'.$typename.'!
自动部署账户: ['.$account['id'].']'.$typename.'('.($account['remark']?$account['remark']:$account['name']).')
关联SSL证书: ['.$row['oid'].']'.implode('、', $domainList).'
任务备注: '.($row['remark']?$row['remark']:'无'); + } else { + $mail_content = '尊敬的用户,您好:您的SSL证书部署失败!
失败原因: '.$row['error'].'
自动部署账户: ['.$account['id'].']'.$typename.'('.($account['remark']?$account['remark']:$account['name']).')
关联SSL证书: ['.$row['oid'].']'.implode('、', $domainList).'
任务备注: '.($row['remark']?$row['remark']:'无'); + } + $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s').''; + + self::cert_send($mail_title, $mail_content, $result); + Db::name('cert_deploy')->where('id', $id)->update(['issend' => 1]); + } + + private static function cert_send($mail_title, $mail_content, $result) + { + if (config_get('cert_notice_mail') == 1 || config_get('cert_notice_mail') == 2 && !$result) { + $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); + self::send_mail($mail_name, $mail_title, $mail_content); + } + if (config_get('cert_notice_wxtpl') == 1 || config_get('cert_notice_wxtpl') == 2 && !$result) { + $content = str_replace(['
', '', ''], ["\n\n", '**', '**'], $mail_content); + self::send_wechat_tplmsg($mail_title, strip_tags($content)); + } + if (config_get('cert_notice_tgbot') == 1 || config_get('cert_notice_tgbot') == 2 && !$result) { + $content = str_replace('
', "\n", $mail_content); + $content = "".$mail_title."\n".strip_tags($content); + self::send_telegram_bot($content); + } + if (config_get('cert_notice_webhook') == 1) { + $content = str_replace(['*', '
', '', ''], ['\*', "\n", '**', '**'], $mail_content); + self::send_webhook($mail_title, $content); + } + } + + public static function expire_notice_send($day, $list) + { + $mail_title = '您有'.count($list).'个域名即将在'.$day.'天后到期'; + $mail_content = '尊敬的用户,您好:您有'.count($list).'个域名即将在'.$day.'天后到期!
域名&到期时间:
'; + foreach ($list as $domain) { + $mail_content .= ''.$domain['name'].' - '.$domain['expiretime'].'
'; + } + $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s').''; + + if (config_get('expire_notice_mail') == 1 || config_get('expire_notice_mail') == 2) { + $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); + self::send_mail($mail_name, $mail_title, $mail_content); + } + if (config_get('expire_notice_wxtpl') == 1 || config_get('expire_notice_wxtpl') == 2) { + $content = str_replace(['
', '', ''], ["\n\n", '**', '**'], $mail_content); + self::send_wechat_tplmsg($mail_title, strip_tags($content)); + } + if (config_get('expire_notice_tgbot') == 1 || config_get('expire_notice_tgbot') == 2) { + $content = str_replace('
', "\n", $mail_content); + $content = "".$mail_title."\n".strip_tags($content); + self::send_telegram_bot($content); + } + if (config_get('expire_notice_webhook') == 1) { + $content = str_replace(['*', '
', '', ''], ['\*', "\n", '**', '**'], $mail_content); + self::send_webhook($mail_title, $content); + } + } + + public static function send_mail($to, $sub, $msg) + { + $mail_type = config_get('mail_type'); + if ($mail_type == 1) { + $mail = new \app\lib\mail\Sendcloud(config_get('mail_apiuser'), config_get('mail_apikey')); + return $mail->send($to, $sub, $msg, config_get('mail_name'), self::$sitename); + } elseif ($mail_type == 2) { + $mail = new \app\lib\mail\Aliyun(config_get('mail_apiuser'), config_get('mail_apikey')); + return $mail->send($to, $sub, $msg, config_get('mail_name'), self::$sitename); + } else { + $mail_name = config_get('mail_name'); + $mail_port = intval(config_get('mail_port')); + $mail_smtp = config_get('mail_smtp'); + $mail_pwd = config_get('mail_pwd'); + if (!$mail_name || !$mail_port || !$mail_smtp || !$mail_pwd) return false; + $mail = new PHPMailer(true); + $mail->setLanguage('zh_cn'); + try { + $mail->SMTPDebug = 0; + $mail->CharSet = 'UTF-8'; + $mail->Timeout = 5; + $mail->isSMTP(); + $mail->Host = $mail_smtp; + $mail->SMTPAuth = true; + $mail->Username = $mail_name; + $mail->Password = $mail_pwd; + if ($mail_port == 587) $mail->SMTPSecure = 'tls'; + else if ($mail_port >= 465) $mail->SMTPSecure = 'ssl'; + else $mail->SMTPAutoTLS = false; + $mail->Port = $mail_port; + $mail->setFrom($mail_name, self::$sitename); + $mail->addAddress($to); + $mail->addReplyTo($mail_name, self::$sitename); + $mail->isHTML(true); + $mail->Subject = $sub; + $mail->Body = $msg; + $mail->send(); + return true; + } catch (\Exception $e) { + return $mail->ErrorInfo; + } + } + } + + public static function send_wechat_tplmsg($title, $content) + { + $wechat_apptoken = config_get('wechat_apptoken'); + $wechat_appuid = config_get('wechat_appuid'); + if (!$wechat_apptoken || !$wechat_appuid) return false; + $url = 'https://wxpusher.zjiecode.com/api/send/message'; + $post = ['appToken' => $wechat_apptoken, 'content' => $content, 'summary' => $title, 'contentType' => 3, 'uids' => [$wechat_appuid]]; + $result = get_curl($url, json_encode($post), 0, 0, 0, 0, ['Content-Type' => 'application/json; charset=UTF-8']); + $arr = json_decode($result, true); + if (isset($arr['success']) && $arr['success'] == true) { + return true; + } else { + return $arr['msg'] ?? '请求失败'; + } + } + + public static function send_telegram_bot($content) + { + $tgbot_token = config_get('tgbot_token'); + $tgbot_chatid = config_get('tgbot_chatid'); + if (!$tgbot_token || !$tgbot_chatid) return false; + $tgbot_url = 'https://api.telegram.org'; + if (config_get('tgbot_proxy') == 2) { + $tgbot_url_n = config_get('tgbot_url'); + if (!empty($tgbot_url_n)) { + $tgbot_url = rtrim($tgbot_url_n, '/'); + } + } + $url = $tgbot_url.'/bot'.$tgbot_token.'/sendMessage'; + $post = ['chat_id' => $tgbot_chatid, 'text' => $content, 'parse_mode' => 'HTML']; + $result = self::telegram_curl($url, http_build_query($post)); + $arr = json_decode($result, true); + if (isset($arr['ok']) && $arr['ok'] == true) { + return true; + } else { + return $arr['description'] ?? '请求失败'; + } + } + + public static function send_webhook($title, $content) + { + $url = config_get('webhook_url'); + if (!$url || !parse_url($url)) return false; + if (strpos($url, 'oapi.dingtalk.com')) { + $content = '### '.$title." \n ".str_replace("\n", " \n ", $content); + $post = [ + 'msgtype' => 'markdown', + 'markdown' => [ + 'title' => $title, + 'text' => $content, + ], + ]; + } elseif (strpos($url, 'qyapi.weixin.qq.com')) { + $content = '## '.$title."\n".$content; + $post = [ + 'msgtype' => 'markdown', + 'markdown' => [ + 'content' => $content, + ], + ]; + } elseif (strpos($url, 'open.feishu.cn') || strpos($url, 'open.larksuite.com')) { + $content = str_replace(['\*', '**'], ['*', ''], strip_tags($content)); + $post = [ + 'msg_type' => 'text', + 'content' => [ + 'text' => $content, + ], + ]; + } else { + return '不支持的Webhook地址'; + } + $result = get_curl($url, json_encode($post), 0, 0, 0, 0, ['Content-Type' => 'application/json; charset=UTF-8']); + $arr = json_decode($result, true); + if (isset($arr['errcode']) && $arr['errcode'] == 0 || isset($arr['code']) && $arr['code'] == 0) { + return true; + } else { + return $arr['errmsg'] ?? (isset($arr['msg']) ? $arr['msg'] : '请求失败'); + } + } + + private static function telegram_curl($url, $post) + { + $ch = curl_init(); + if (config_get('tgbot_proxy') == 1) { + curl_set_proxy($ch); + } + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + $httpheader[] = "Accept: */*"; + $httpheader[] = "Accept-Encoding: gzip,deflate,sdch"; + $httpheader[] = "Accept-Language: zh-CN,zh;q=0.8"; + $httpheader[] = "Connection: close"; + curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post); + curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Linux; U; Android 4.0.4; es-mx; HTC_One_X Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0"); + curl_setopt($ch, CURLOPT_ENCODING, "gzip"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $ret = curl_exec($ch); + curl_close($ch); + return $ret; + } +} diff --git a/app/view/auth/login.html b/app/view/auth/login.html index ce537e5..bae3da2 100644 --- a/app/view/auth/login.html +++ b/app/view/auth/login.html @@ -1,180 +1,180 @@ - - - - - - - - 聚合DNS管理系统 - 登录 - - - - - - - - - - -
- -
- - - - - + + + + + + + + 聚合DNS管理系统 - 登录 + + + + + + + + + + +
+ +
+ + + + + \ No newline at end of file diff --git a/app/view/cert/account_form.html b/app/view/cert/account_form.html index 441bea7..9e6113f 100644 --- a/app/view/cert/account_form.html +++ b/app/view/cert/account_form.html @@ -1,340 +1,340 @@ -{extend name="common/layout" /} -{block name="title"}{$title}{/block} -{block name="main"} - -
-
-
-

返回{if $action=='edit'}编辑{else}添加{/if}{$title}

-
- -
-
- - -
-
- - -
-
- -
-
- {{ typeList[set.type].name }} - 重新选择 -
- -
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-
- -
- -
-
-
-
-
- - 提示: -
-
-
-
-
-
-
-
-
-{/block} -{block name="script"} - - - - -{/block} +{extend name="common/layout" /} +{block name="title"}{$title}{/block} +{block name="main"} + +
+
+
+

返回{if $action=='edit'}编辑{else}添加{/if}{$title}

+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ {{ typeList[set.type].name }} + 重新选择 +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+ + 提示: +
+
+
+
+
+
+
+
+
+{/block} +{block name="script"} + + + + +{/block} diff --git a/app/view/cert/certaccount.html b/app/view/cert/certaccount.html index 776affa..4f599dc 100644 --- a/app/view/cert/certaccount.html +++ b/app/view/cert/certaccount.html @@ -1,93 +1,93 @@ -{extend name="common/layout" /} -{block name="title"}SSL证书账户管理{/block} -{block name="main"} -
-
-
-
- -
-
- - -
- - 刷新 - 添加 -
- - -
-
-
-
-
-{/block} -{block name="script"} - - - - - +{extend name="common/layout" /} +{block name="title"}SSL证书账户管理{/block} +{block name="main"} +
+
+
+
+ +
+
+ + +
+ + 刷新 + 添加 +
+ + +
+
+
+
+
+{/block} +{block name="script"} + + + + + {/block} \ No newline at end of file diff --git a/app/view/cert/certorder.html b/app/view/cert/certorder.html index 12bbee0..923da4a 100644 --- a/app/view/cert/certorder.html +++ b/app/view/cert/certorder.html @@ -1,472 +1,472 @@ -{extend name="common/layout" /} -{block name="title"}SSL证书订单列表{/block} -{block name="main"} - -
-
-
-
- -
- - -
- -
- -
-
-
- -
-
- -
- - 刷新 -
- 添加 -
- - -
- - -
-
-
-
-
-{/block} -{block name="script"} - - - - - - +{extend name="common/layout" /} +{block name="title"}SSL证书订单列表{/block} +{block name="main"} + +
+
+
+
+ +
+ + +
+ +
+ +
+
+
+ +
+
+ +
+ + 刷新 +
+ 添加 +
+ + +
+ + +
+
+
+
+
+{/block} +{block name="script"} + + + + + + {/block} \ No newline at end of file diff --git a/app/view/cert/certset.html b/app/view/cert/certset.html index 6b97073..1df7949 100644 --- a/app/view/cert/certset.html +++ b/app/view/cert/certset.html @@ -1,112 +1,112 @@ -{extend name="common/layout" /} -{block name="title"}SSL证书计划任务{/block} -{block name="main"} -
-
- -
-

自动续签设置

-
-
-
- -
-
-
-
- -
-
-
-
- -
- -
-

自动部署设置

-
-
-
- -
-
-
-
- -
-
-
-
-
- -
-

通知设置

-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
- -
- -
-
-{/block} -{block name="script"} - - +{extend name="common/layout" /} +{block name="title"}SSL证书计划任务{/block} +{block name="main"} +
+
+ +
+

自动续签设置

+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+ +
+

自动部署设置

+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+

通知设置

+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ +
+ +
+
+{/block} +{block name="script"} + + {/block} \ No newline at end of file diff --git a/app/view/cert/cname.html b/app/view/cert/cname.html index 9393d4f..2acc283 100644 --- a/app/view/cert/cname.html +++ b/app/view/cert/cname.html @@ -1,245 +1,245 @@ -{extend name="common/layout" /} -{block name="title"}CMAME代理记录管理{/block} -{block name="main"} - - -

CNAME代理可以让未在本系统添加的域名自动申请SSL证书,支持所有DNS服务商。

注:仅支持基于ACME的证书类型,不支持腾讯云等云厂商SSL证书。

-
-
-
-
- -
-
- - -
- - 刷新 - 添加 -
- - -
-
-
-
-
-{/block} -{block name="script"} - - - - - - +{extend name="common/layout" /} +{block name="title"}CMAME代理记录管理{/block} +{block name="main"} + + +

CNAME代理可以让未在本系统添加的域名自动申请SSL证书,支持所有DNS服务商。

注:仅支持基于ACME的证书类型,不支持腾讯云等云厂商SSL证书。

+
+
+
+
+ +
+
+ + +
+ + 刷新 + 添加 +
+ + +
+
+
+
+
+{/block} +{block name="script"} + + + + + + {/block} \ No newline at end of file diff --git a/app/view/cert/deploy_form.html b/app/view/cert/deploy_form.html index fab6bb9..adccf96 100644 --- a/app/view/cert/deploy_form.html +++ b/app/view/cert/deploy_form.html @@ -1,255 +1,255 @@ -{extend name="common/layout" /} -{block name="title"}自动部署任务{/block} -{block name="main"} - -
-
-
-

返回{if $action=='edit'}编辑{else}添加{/if}自动部署任务

-
-
-
- -
-
-
- -
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-
- -
- -
-
-
-
-
- - 提示: -
-
-
-
-
-
-
-
-
-{/block} -{block name="script"} - - - - - - +{extend name="common/layout" /} +{block name="title"}自动部署任务{/block} +{block name="main"} + +
+
+
+

返回{if $action=='edit'}编辑{else}添加{/if}自动部署任务

+
+
+
+ +
+
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+ + 提示: +
+
+
+
+
+
+
+
+
+{/block} +{block name="script"} + + + + + + {/block} \ No newline at end of file diff --git a/app/view/cert/deployaccount.html b/app/view/cert/deployaccount.html index 41d3dcf..509e898 100644 --- a/app/view/cert/deployaccount.html +++ b/app/view/cert/deployaccount.html @@ -1,93 +1,93 @@ -{extend name="common/layout" /} -{block name="title"}自动部署任务账户管理{/block} -{block name="main"} -
-
-
-
- -
-
- - -
- - 刷新 - 添加 -
- - -
-
-
-
-
-{/block} -{block name="script"} - - - - - +{extend name="common/layout" /} +{block name="title"}自动部署任务账户管理{/block} +{block name="main"} +
+
+
+
+ +
+
+ + +
+ + 刷新 + 添加 +
+ + +
+
+
+
+
+{/block} +{block name="script"} + + + + + {/block} \ No newline at end of file diff --git a/app/view/cert/deploytask.html b/app/view/cert/deploytask.html index 9a38fd7..17c04ff 100644 --- a/app/view/cert/deploytask.html +++ b/app/view/cert/deploytask.html @@ -1,356 +1,356 @@ -{extend name="common/layout" /} -{block name="title"}SSL证书自动部署任务{/block} -{block name="main"} - -
-
-
-
- -
- - -
- -
- -
-
-
-
- -
-
-
- -
-
- -
- - 刷新 - 添加 - -
- - -
-
-
-
-
-{/block} -{block name="script"} - - - - - - +{extend name="common/layout" /} +{block name="title"}SSL证书自动部署任务{/block} +{block name="main"} + +
+
+
+
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+
+ +
+
+ +
+ + 刷新 + 添加 + +
+ + +
+
+
+
+
+{/block} +{block name="script"} + + + + + + {/block} \ No newline at end of file diff --git a/app/view/cert/order_form.html b/app/view/cert/order_form.html index 442eaaa..6148aae 100644 --- a/app/view/cert/order_form.html +++ b/app/view/cert/order_form.html @@ -1,195 +1,195 @@ -{extend name="common/layout" /} -{block name="title"}SSL证书订单{/block} -{block name="main"} - -
-
-
-

返回{if $action=='edit'}修改{else}添加{/if}SSL证书订单

-
-
-
- -
-
-
- -
-
- - -
-
-
-
- -
-
- - -
-
-
-
- -
- -
-
-
- -
- -
-
- -
- -
- -
-
- -
-
-
-

提示:添加或修改订单信息,点击提交后,不会立即执行签发,只能通过计划任务或列表手动点击来执行

证书签发之前确保该主域名下没有CAA类型记录,避免证书验证失败。

-

提示:选择手动续期,到达设置的续期天数,只会发送消息通知。

-
-
-
-{/block} -{block name="script"} - - - - +{extend name="common/layout" /} +{block name="title"}SSL证书订单{/block} +{block name="main"} + +
+
+
+

返回{if $action=='edit'}修改{else}添加{/if}SSL证书订单

+
+
+
+ +
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+

提示:添加或修改订单信息,点击提交后,不会立即执行签发,只能通过计划任务或列表手动点击来执行

证书签发之前确保该主域名下没有CAA类型记录,避免证书验证失败。

+

提示:选择手动续期,到达设置的续期天数,只会发送消息通知。

+
+
+
+{/block} +{block name="script"} + + + + {/block} \ No newline at end of file diff --git a/app/view/common/layout.html b/app/view/common/layout.html index db40085..888903b 100644 --- a/app/view/common/layout.html +++ b/app/view/common/layout.html @@ -1,257 +1,257 @@ - - - - - - - - {block name="title"}标题{/block} - - - - - - - {if file_exists(public_path() . 'static/css/custom.css')} - - {/if} - - - - - - -
-
- - - - -
- - - - -
- -
-{block name="main"}主内容{/block} -
- -
- - - - -
-
- - - - - - -{block name="script"}{/block} - + + + + + + + + {block name="title"}标题{/block} + + + + + + + {if file_exists(public_path() . 'static/css/custom.css')} + + {/if} + + + + + + +
+
+ + + + +
+ + + + +
+ +
+{block name="main"}主内容{/block} +
+ +
+ + + + +
+
+ + + + + + +{block name="script"}{/block} + \ No newline at end of file diff --git a/app/view/dispatch_jump.html b/app/view/dispatch_jump.html index 7e7452b..69d6246 100644 --- a/app/view/dispatch_jump.html +++ b/app/view/dispatch_jump.html @@ -1,60 +1,60 @@ - - - - - 温馨提示 - - - - - -
-
- -
-

{$msg}

- {if $url} -

- 页面将在 {$wait} 秒后自动跳转 -

- {/if} -

- 返回上一页 - {if $url} - 立即跳转 - {/if} -

-
- - + + + + + 温馨提示 + + + + + +
+
+ +
+

{$msg}

+ {if $url} +

+ 页面将在 {$wait} 秒后自动跳转 +

+ {/if} +

+ 返回上一页 + {if $url} + 立即跳转 + {/if} +

+
+ + \ No newline at end of file diff --git a/app/view/dmonitor/overview.html b/app/view/dmonitor/overview.html index bdeff4a..d2e7fa8 100644 --- a/app/view/dmonitor/overview.html +++ b/app/view/dmonitor/overview.html @@ -1,212 +1,212 @@ -{extend name="common/layout" /} -{block name="title"}容灾切换运行概览{/block} -{block name="main"} - - - -
-
-
- - -
- 运行状态 - {$info.run_state==1?'正在运行':'已停止'} -
- -
- -
- -
-
- - -
- 今日运行次数 - {$info.run_count} -
- -
- -
- - - -
- -
-
- - -
- 24H告警次数 - {$info.fail_count} -
- -
- -
- -
-
- - -
- 24H切换次数 - {$info.switch_count} -
- -
- -
- -
-
-
-
-

运行概览

-
-
  • 上次运行时间: {$info.run_time}
  • -
  • 当前时间: {:date('Y-m-d H:i:s')}
  • -
  • Swoole组件: {$info.swoole|raw}
  • - {if $info.run_error}
  • 上次运行错误信息: {$info.run_error}
  • {/if} - -
    -
    -
    -
    -
    -

    操作说明

    -
    -

    1、php需要安装swoole组件

    -

    2、在命令行执行以下命令启动进程:

    -

    cd {:app()->getRootPath()} && php think dmtask

    -

    3、也可以使用进程守护管理器,添加守护进程。
    运行目录:{:app()->getRootPath()}
    启动命令:php think dmtask

    -
    -
    -
    -
    -{/block} -{block name="script"} - - +{extend name="common/layout" /} +{block name="title"}容灾切换运行概览{/block} +{block name="main"} + + + +
    +
    +
    + + +
    + 运行状态 + {$info.run_state==1?'正在运行':'已停止'} +
    + +
    + +
    + +
    +
    + + +
    + 今日运行次数 + {$info.run_count} +
    + +
    + +
    + + + +
    + +
    +
    + + +
    + 24H告警次数 + {$info.fail_count} +
    + +
    + +
    + +
    +
    + + +
    + 24H切换次数 + {$info.switch_count} +
    + +
    + +
    + +
    +
    +
    +
    +

    运行概览

    +
    +
  • 上次运行时间: {$info.run_time}
  • +
  • 当前时间: {:date('Y-m-d H:i:s')}
  • +
  • Swoole组件: {$info.swoole|raw}
  • + {if $info.run_error}
  • 上次运行错误信息: {$info.run_error}
  • {/if} + +
    +
    +
    +
    +
    +

    操作说明

    +
    +

    1、php需要安装swoole组件

    +

    2、在命令行执行以下命令启动进程:

    +

    cd {:app()->getRootPath()} && php think dmtask

    +

    3、也可以使用进程守护管理器,添加守护进程。
    运行目录:{:app()->getRootPath()}
    启动命令:php think dmtask

    +
    +
    +
    +
    +{/block} +{block name="script"} + + {/block} \ No newline at end of file diff --git a/app/view/dmonitor/task.html b/app/view/dmonitor/task.html index b05a4aa..1691d7c 100644 --- a/app/view/dmonitor/task.html +++ b/app/view/dmonitor/task.html @@ -1,233 +1,233 @@ -{extend name="common/layout" /} -{block name="title"}容灾切换策略{/block} -{block name="main"} - -
    -
    -
    -
    - -
    -
    - -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    - - 刷新 - 添加 - -
    - - -
    -
    -
    -
    -
    -{/block} -{block name="script"} - - - - - +{extend name="common/layout" /} +{block name="title"}容灾切换策略{/block} +{block name="main"} + +
    +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + + 刷新 + 添加 + +
    + + +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + {/block} \ No newline at end of file diff --git a/app/view/dmonitor/taskform.html b/app/view/dmonitor/taskform.html index aa6807d..99a33e6 100644 --- a/app/view/dmonitor/taskform.html +++ b/app/view/dmonitor/taskform.html @@ -1,297 +1,297 @@ -{extend name="common/layout" /} -{block name="title"}容灾切换策略{/block} -{block name="main"} - -
    -
    -
    -

    返回{if $action=='edit'}编辑{else}添加{/if}容灾切换策略

    -
    -
    -
    - -
    -
    - - . - -
    -
    -
    -
    - -
    - -
    - -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -{/block} -{block name="script"} - - - - +{extend name="common/layout" /} +{block name="title"}容灾切换策略{/block} +{block name="main"} + +
    +
    +
    +

    返回{if $action=='edit'}编辑{else}添加{/if}容灾切换策略

    +
    +
    +
    + +
    +
    + + . + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + + +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + {/block} \ No newline at end of file diff --git a/app/view/dmonitor/taskinfo.html b/app/view/dmonitor/taskinfo.html index 2dd0bdf..57aa845 100644 --- a/app/view/dmonitor/taskinfo.html +++ b/app/view/dmonitor/taskinfo.html @@ -1,73 +1,73 @@ -{extend name="common/layout" /} -{block name="title"}切换记录{/block} -{block name="main"} - -
    -
    -
    -
    - -
    -
    - -
    - -
    -
    - - 刷新 -   24H告警次数:{$info.fail_count}  切换次数:{$info.switch_count} -
    - - -
    -
    -
    -
    -
    -{/block} -{block name="script"} - - - - - +{extend name="common/layout" /} +{block name="title"}切换记录{/block} +{block name="main"} + +
    +
    +
    +
    + +
    +
    + +
    + +
    +
    + + 刷新 +   24H告警次数:{$info.fail_count}  切换次数:{$info.switch_count} +
    + + +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + {/block} \ No newline at end of file diff --git a/app/view/domain/account.html b/app/view/domain/account.html index e050312..b1c3f13 100644 --- a/app/view/domain/account.html +++ b/app/view/domain/account.html @@ -1,248 +1,248 @@ -{extend name="common/layout" /} -{block name="title"}域名账户{/block} -{block name="main"} - -
    -
    -
    -
    - -
    -
    - - -
    - - 刷新 - 添加 -
    - - -
    -
    -
    -
    -
    -{/block} -{block name="script"} - - - - - +{extend name="common/layout" /} +{block name="title"}域名账户{/block} +{block name="main"} + +
    +
    +
    +
    + +
    +
    + + +
    + + 刷新 + 添加 +
    + + +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + {/block} \ No newline at end of file diff --git a/app/view/domain/batchadd.html b/app/view/domain/batchadd.html index 1c7cfba..a36599a 100644 --- a/app/view/domain/batchadd.html +++ b/app/view/domain/batchadd.html @@ -1,138 +1,138 @@ -{extend name="common/layout" /} -{block name="title"}批量添加解析 - {$domainName}{/block} -{block name="main"} -
    -
    -
    -

    返回批量添加解析 - {$domainName}

    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    -
    -
    - -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -{/block} -{block name="script"} - - - +{extend name="common/layout" /} +{block name="title"}批量添加解析 - {$domainName}{/block} +{block name="main"} +
    +
    +
    +

    返回批量添加解析 - {$domainName}

    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + {/block} \ No newline at end of file diff --git a/app/view/domain/batchadd2.html b/app/view/domain/batchadd2.html index 9d6d6a3..91be23f 100644 --- a/app/view/domain/batchadd2.html +++ b/app/view/domain/batchadd2.html @@ -1,176 +1,176 @@ -{extend name="common/layout" /} -{block name="title"}批量添加解析{/block} -{block name="main"} - -
    -
    -
    -

    返回批量添加解析

    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - - -
    -
    - -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    解析记录添加结果

    -
    - - - - - - - - - - - - - - - -
    ID域名添加结果
    {{item.id}}{{item.name}}
    -
    -
    -{/block} -{block name="script"} - - - +{extend name="common/layout" /} +{block name="title"}批量添加解析{/block} +{block name="main"} + +
    +
    +
    +

    返回批量添加解析

    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + + +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    解析记录添加结果

    +
    + + + + + + + + + + + + + + + +
    ID域名添加结果
    {{item.id}}{{item.name}}
    +
    +
    +{/block} +{block name="script"} + + + {/block} \ No newline at end of file diff --git a/app/view/domain/batchedit.html b/app/view/domain/batchedit.html index 5122bf1..743ea43 100644 --- a/app/view/domain/batchedit.html +++ b/app/view/domain/batchedit.html @@ -1,157 +1,157 @@ -{extend name="common/layout" /} -{block name="title"}批量修改解析{/block} -{block name="main"} - -
    -
    -
    -

    返回批量修改解析

    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    - -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    解析记录修改结果

    -
    - - - - - - - - - - - - - - - -
    ID域名修改结果
    {{item.id}}{{item.name}}
    -
    -
    -{/block} -{block name="script"} - - - +{extend name="common/layout" /} +{block name="title"}批量修改解析{/block} +{block name="main"} + +
    +
    +
    +

    返回批量修改解析

    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    解析记录修改结果

    +
    + + + + + + + + + + + + + + + +
    ID域名修改结果
    {{item.id}}{{item.name}}
    +
    +
    +{/block} +{block name="script"} + + + {/block} \ No newline at end of file diff --git a/app/view/domain/domain.html b/app/view/domain/domain.html index 48430a8..8054be0 100644 --- a/app/view/domain/domain.html +++ b/app/view/domain/domain.html @@ -1,620 +1,620 @@ -{extend name="common/layout" /} -{block name="title"}域名管理{/block} -{block name="main"} - - - -
    -
    -
    -
    - -
    -
    - - -
    -
    - -
    -
    - -
    - - 刷新 - {if request()->user['level'] eq 2} 添加 - - 到期提醒设置{/if} -
    - - -
    -
    -
    -
    -
    -{/block} -{block name="script"} - - - - - - - - - - -{/block} +{extend name="common/layout" /} +{block name="title"}域名管理{/block} +{block name="main"} + + + +
    +
    +
    +
    + +
    +
    + + +
    +
    + +
    +
    + +
    + + 刷新 + {if request()->user['level'] eq 2} 添加 + + 到期提醒设置{/if} +
    + + +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + + + + + + +{/block} diff --git a/app/view/domain/domain_add.html b/app/view/domain/domain_add.html index dc09085..854f194 100644 --- a/app/view/domain/domain_add.html +++ b/app/view/domain/domain_add.html @@ -1,158 +1,158 @@ -{extend name="common/layout" /} -{block name="title"}批量添加域名{/block} -{block name="main"} -
    -
    -
    -

    返回批量添加域名

    -
    -
    -
    - -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    - - - - - - - - - - - - - -
    域名
    {{item.Domain}} (已添加)
    -
    -
    -
    -
    -
    -
    -
    -
    -{/block} -{block name="script"} - - - +{extend name="common/layout" /} +{block name="title"}批量添加域名{/block} +{block name="main"} +
    +
    +
    +

    返回批量添加域名

    +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + + + + + + + + + + + + + +
    域名
    {{item.Domain}} (已添加)
    +
    +
    +
    +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + {/block} \ No newline at end of file diff --git a/app/view/domain/expire_notice.html b/app/view/domain/expire_notice.html index 91b8778..7057a28 100644 --- a/app/view/domain/expire_notice.html +++ b/app/view/domain/expire_notice.html @@ -1,81 +1,81 @@ -{extend name="common/layout" /} -{block name="title"}域名到期提醒设置{/block} -{block name="main"} -
    -
    - -
    -

    返回域名到期提醒设置

    -
    -
    -
    - -
    域名到期前多少天发送通知,可填写多个天数,用英文逗号隔开。例如填写7,14则在域名到期前7天与14天分别发送通知。
    -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - -
    - -
    -
    -{/block} -{block name="script"} - - +{extend name="common/layout" /} +{block name="title"}域名到期提醒设置{/block} +{block name="main"} +
    +
    + +
    +

    返回域名到期提醒设置

    +
    +
    +
    + +
    域名到期前多少天发送通知,可填写多个天数,用英文逗号隔开。例如填写7,14则在域名到期前7天与14天分别发送通知。
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +{/block} +{block name="script"} + + {/block} \ No newline at end of file diff --git a/app/view/domain/log.html b/app/view/domain/log.html index c677f2d..cef45c2 100644 --- a/app/view/domain/log.html +++ b/app/view/domain/log.html @@ -1,47 +1,47 @@ -{extend name="common/layout" /} -{block name="title"}域名日志{/block} -{block name="main"} - -
    -
    -
    -
    - - -
    -
    -
    -
    -
    -{/block} -{block name="script"} - - - - - +{extend name="common/layout" /} +{block name="title"}域名日志{/block} +{block name="main"} + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + {/block} \ No newline at end of file diff --git a/app/view/domain/record.html b/app/view/domain/record.html index fe95a77..10726b3 100644 --- a/app/view/domain/record.html +++ b/app/view/domain/record.html @@ -1,768 +1,768 @@ -{extend name="common/layout" /} -{block name="title"}解析管理 - {$domainName}{/block} -{block name="main"} - - - - -
    -
    -
    -
    -

    {if request()->user['type'] eq 'user'} 返回{/if}{$domainName}

    -
    -
    - -
    -
    -
    - - -
    -
    - -
    - - 刷新 - 添加记录 - {if $dnsconfig.type=='aliyun'}权重配置{/if} -
    - - -
    -
    - - -
    - 高级搜索 -
    - -
    - - -
    -
    -
    -
    -
    -{/block} -{block name="script"} - - - - - - +{extend name="common/layout" /} +{block name="title"}解析管理 - {$domainName}{/block} +{block name="main"} + + + + +
    +
    +
    +
    +

    {if request()->user['type'] eq 'user'} 返回{/if}{$domainName}

    +
    +
    + +
    +
    +
    + + +
    +
    + +
    + + 刷新 + 添加记录 + {if $dnsconfig.type=='aliyun'}权重配置{/if} +
    + + +
    +
    + + +
    + 高级搜索 +
    + +
    + + +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + + {/block} \ No newline at end of file diff --git a/app/view/domain/weight.html b/app/view/domain/weight.html index 02c592e..ce06f10 100644 --- a/app/view/domain/weight.html +++ b/app/view/domain/weight.html @@ -1,252 +1,252 @@ -{extend name="common/layout" /} -{block name="title"}权重配置 - {$domainName}{/block} -{block name="main"} - - -
    -
    -
    -

    返回权重配置 - {$domainName}

    -
    - -
    -
    - - -
    - - 刷新 -
    - - -
    -
    -
    -
    -
    -{/block} -{block name="script"} - - - - - +{extend name="common/layout" /} +{block name="title"}权重配置 - {$domainName}{/block} +{block name="main"} + + +
    +
    +
    +

    返回权重配置 - {$domainName}

    +
    + +
    +
    + + +
    + + 刷新 +
    + + +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + {/block} \ No newline at end of file diff --git a/app/view/exception.tpl b/app/view/exception.tpl index 302f24e..d2fdfc2 100644 --- a/app/view/exception.tpl +++ b/app/view/exception.tpl @@ -492,7 +492,7 @@ if (!function_exists('echo_value')) { } })(); - $.getScript('//cdn.bootcdn.net/ajax/libs/prettify/r298/prettify.min.js', function(){ + $.getScript('/static/js/prettify.min.js', function(){ prettyPrint(); }); })(); diff --git a/app/view/index/index.html b/app/view/index/index.html index 3d0a7e6..77575d6 100644 --- a/app/view/index/index.html +++ b/app/view/index/index.html @@ -1,240 +1,240 @@ -{extend name="common/layout" /} -{block name="title"}聚合DNS管理系统{/block} -{block name="main"} - -
    -
    - -
    -
    -

    0

    -

    域名数量

    -
    -
    - -
    - More info -
    -
    - -
    - -
    -
    -

    0

    -

    容灾切换策略

    -
    -
    - -
    - More info -
    -
    - -
    - -
    -
    -

    0

    -

    SSL证书订单

    -
    -
    - -
    - More info -
    -
    - -
    - -
    -
    -

    0

    -

    SSL部署任务

    -
    -
    - -
    - More info -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    -
    - -

    CF优选IP概览

    -
    - -
    -
    - -
    -
    -
    - -

    服务器信息

    -
    - - - - - - - - - - - - - - - - - - - - - - - -
    框架版本{$info.framework_version}
    PHP版本{$info.php_version}
    数据库版本{$info.mysql_version}
    Web服务器{$info.software}
    服务器时间{$info.date}
    -
    -
    -
    - -

    版本信息

    -
    -
      -
      -
      -
      -
      -{/block} -{block name="script"} - - - +{extend name="common/layout" /} +{block name="title"}聚合DNS管理系统{/block} +{block name="main"} + +
      +
      + +
      +
      +

      0

      +

      域名数量

      +
      +
      + +
      + More info +
      +
      + +
      + +
      +
      +

      0

      +

      容灾切换策略

      +
      +
      + +
      + More info +
      +
      + +
      + +
      +
      +

      0

      +

      SSL证书订单

      +
      +
      + +
      + More info +
      +
      + +
      + +
      +
      +

      0

      +

      SSL部署任务

      +
      +
      + +
      + More info +
      +
      + +
      + +
      +
      +
      +
      +
      + +
      +
      + +

      CF优选IP概览

      +
      + +
      +
      + +
      +
      +
      + +

      服务器信息

      +
      + + + + + + + + + + + + + + + + + + + + + + + +
      框架版本{$info.framework_version}
      PHP版本{$info.php_version}
      数据库版本{$info.mysql_version}
      Web服务器{$info.software}
      服务器时间{$info.date}
      +
      +
      +
      + +

      版本信息

      +
      +
        +
        +
        +
        +
        +{/block} +{block name="script"} + + + {/block} \ No newline at end of file diff --git a/app/view/index/setpwd.html b/app/view/index/setpwd.html index 9b4f198..a8740af 100644 --- a/app/view/index/setpwd.html +++ b/app/view/index/setpwd.html @@ -1,187 +1,187 @@ -{extend name="common/layout" /} -{block name="title"}修改密码{/block} -{block name="main"} -
        -
        -
        -

        修改密码

        -
        -
        -
        - - -
        -
        - - -
        -
        - - -
        -
        - -
        -
        -
        -
        -
        -

        TOTP二次验证

        -
        -
        -
        -
        - {if $user.totp_open == 1} - -
        -
        - {else} - -
        - {/if} -
        -
        -
        -
        - -
        - -{/block} -{block name="script"} - - - - +{extend name="common/layout" /} +{block name="title"}修改密码{/block} +{block name="main"} +
        +
        +
        +

        修改密码

        +
        +
        +
        + + +
        +
        + + +
        +
        + + +
        +
        + +
        +
        +
        +
        +
        +

        TOTP二次验证

        +
        +
        +
        +
        + {if $user.totp_open == 1} + +
        +
        + {else} + +
        + {/if} +
        +
        +
        +
        + +
        + +{/block} +{block name="script"} + + + + {/block} \ No newline at end of file diff --git a/app/view/install/index.html b/app/view/install/index.html index 3667912..3aac674 100644 --- a/app/view/install/index.html +++ b/app/view/install/index.html @@ -1,266 +1,266 @@ - - - - - - 聚合DNS管理系统 - 安装程序 - - - - - - -
        -

        - - -

        -

        聚合DNS管理系统 - 安装程序

        -
        - -
        - - - -
        -
        - - -
        - -
        - - -
        - -
        - - -
        - -
        - - -
        - -
        - - -
        - -
        - - -
        -
        - -
        -
        - - -
        - -
        - - -
        -
        - -
        - - - -
        -
        -
        -
        - - - + + + + + + 聚合DNS管理系统 - 安装程序 + + + + + + +
        +

        + + +

        +

        聚合DNS管理系统 - 安装程序

        +
        + +
        + + + +
        +
        + + +
        + +
        + + +
        + +
        + + +
        + +
        + + +
        + +
        + + +
        + +
        + + +
        +
        + +
        +
        + + +
        + +
        + + +
        +
        + +
        + + + +
        +
        +
        +
        + + + \ No newline at end of file diff --git a/app/view/optimizeip/opipform.html b/app/view/optimizeip/opipform.html index b25814e..b3158a4 100644 --- a/app/view/optimizeip/opipform.html +++ b/app/view/optimizeip/opipform.html @@ -1,188 +1,188 @@ -{extend name="common/layout" /} -{block name="title"}优选IP任务{/block} -{block name="main"} - -
        -
        -
        -

        返回{if $action=='edit'}编辑{else}添加{/if}优选IP任务

        -
        -
        -
        - -
        -
        - - . - - -
        -
        -
        -
        - -
        - -
        -
        -
        - -
        - - -
        -
        -
        - -
        -
        -
        -
        -
        - -
        - -
        -
        -
        - -
        - -
        -
        -
        - -
        - -
        -
        -
        -
        -
        - - 提示:所选域名需保留一个默认线路的解析记录,指向{{cdntypeList[set.cdn_type]}}提供的CNAME地址,其他线路的解析记录需要删除,添加任务后将自动为当前域名添加电信/联通/移动线路的解析记录。 -
        -
        -
        -
        -
        -
        - - 提示:所选域名需删除全部解析记录,添加任务后将自动为当前域名添加解析记录。 -
        -
        -
        -
        -
        -
        -
        -
        -
        -{/block} -{block name="script"} - - - - +{extend name="common/layout" /} +{block name="title"}优选IP任务{/block} +{block name="main"} + +
        +
        +
        +

        返回{if $action=='edit'}编辑{else}添加{/if}优选IP任务

        +
        +
        +
        + +
        +
        + + . + + +
        +
        +
        +
        + +
        + +
        +
        +
        + +
        + + +
        +
        +
        + +
        +
        +
        +
        +
        + +
        + +
        +
        +
        + +
        + +
        +
        +
        + +
        + +
        +
        +
        +
        +
        + + 提示:所选域名需保留一个默认线路的解析记录,指向{{cdntypeList[set.cdn_type]}}提供的CNAME地址,其他线路的解析记录需要删除,添加任务后将自动为当前域名添加电信/联通/移动线路的解析记录。 +
        +
        +
        +
        +
        +
        + + 提示:所选域名需删除全部解析记录,添加任务后将自动为当前域名添加解析记录。 +
        +
        +
        +
        +
        +
        +
        +
        +
        +{/block} +{block name="script"} + + + + {/block} \ No newline at end of file diff --git a/app/view/optimizeip/opiplist.html b/app/view/optimizeip/opiplist.html index c5ec43e..8a47945 100644 --- a/app/view/optimizeip/opiplist.html +++ b/app/view/optimizeip/opiplist.html @@ -1,189 +1,189 @@ -{extend name="common/layout" /} -{block name="title"}CF优选IP任务管理{/block} -{block name="main"} - -
        -
        -
        -
        - -
        -
        - -
        - -
        -
        -
        - -
        -
        -
        - -
        -
        - - 刷新 - 添加 -
        - - -
        -
        -
        -
        -
        -{/block} -{block name="script"} - - - - - +{extend name="common/layout" /} +{block name="title"}CF优选IP任务管理{/block} +{block name="main"} + +
        +
        +
        +
        + +
        +
        + +
        + +
        +
        +
        + +
        +
        +
        + +
        +
        + + 刷新 + 添加 +
        + + +
        +
        +
        +
        +
        +{/block} +{block name="script"} + + + + + {/block} \ No newline at end of file diff --git a/app/view/optimizeip/opipset.html b/app/view/optimizeip/opipset.html index 9199c3f..f93f4d0 100644 --- a/app/view/optimizeip/opipset.html +++ b/app/view/optimizeip/opipset.html @@ -1,121 +1,121 @@ -{extend name="common/layout" /} -{block name="title"}CF优选IP设置{/block} -{block name="main"} -
        -
        -
        -

        功能简介

        -
        -

        由于CloudFlare官方IP是泛播路由,同一个IP在不同地区不同运营商所链接的机房是不同的,速度或延迟也会有区别。目前网上也有很多CF优选CNAME服务,然而公共的CNAME可能无法满足稳定性和安全性的需要。

        -

        本功能可以获取CloudFlare最新的优选IP地址(分为电信/联通/移动线路),并自动更新到域名解析记录。

        -
        -
        -
        -

        使用说明

        -
        -

      • 不支持对CloudFlare里的域名添加优选,必须使用其他DNS服务商。需开通Cloudflare for SaaS,且域名使用CNAME的方式解析到CloudFlare。
      • -

      • 数据接口:wetest.vip 数据接口支持CloudFlare、CloudFront、EdgeOne;HostMonit 只支持CloudFlare。
      • -

      • 接口密钥:默认o1zrmHAF为免费KEY可永久免费使用。
      • -

      • 自动更新:可查看计划任务设置

        -
      • -
        -
        -
        -
        -

        数据接口设置

        -
        -
        -
        - -
        -
        -
        - -
        -
        -
        -
        - - 查询积分 -
        -
        -
        -
        -
        -
        -

        自动更新设置

        -
        -
        -
        - -
        -
        -
        -
        - -
        -
        -
        -
        -
        -
        - -
        -{/block} -{block name="script"} - - +{extend name="common/layout" /} +{block name="title"}CF优选IP设置{/block} +{block name="main"} +
        +
        +
        +

        功能简介

        +
        +

        由于CloudFlare官方IP是泛播路由,同一个IP在不同地区不同运营商所链接的机房是不同的,速度或延迟也会有区别。目前网上也有很多CF优选CNAME服务,然而公共的CNAME可能无法满足稳定性和安全性的需要。

        +

        本功能可以获取CloudFlare最新的优选IP地址(分为电信/联通/移动线路),并自动更新到域名解析记录。

        +
        +
        +
        +

        使用说明

        +
        +

      • 不支持对CloudFlare里的域名添加优选,必须使用其他DNS服务商。需开通Cloudflare for SaaS,且域名使用CNAME的方式解析到CloudFlare。
      • +

      • 数据接口:wetest.vip 数据接口支持CloudFlare、CloudFront、EdgeOne;HostMonit 只支持CloudFlare。
      • +

      • 接口密钥:默认o1zrmHAF为免费KEY可永久免费使用。
      • +

      • 自动更新:可查看计划任务设置

        +
      • +
        +
        +
        +
        +

        数据接口设置

        +
        +
        +
        + +
        +
        +
        + +
        +
        +
        +
        + + 查询积分 +
        +
        +
        +
        +
        +
        +

        自动更新设置

        +
        +
        +
        + +
        +
        +
        +
        + +
        +
        +
        +
        +
        +
        + +
        +{/block} +{block name="script"} + + {/block} \ No newline at end of file diff --git a/app/view/schedule/stask.html b/app/view/schedule/stask.html index 60f6bc3..82a2824 100644 --- a/app/view/schedule/stask.html +++ b/app/view/schedule/stask.html @@ -1,216 +1,216 @@ -{extend name="common/layout" /} -{block name="title"}定时切换策略{/block} -{block name="main"} - -
        -
        -
        -
        - -
        -
        - -
        - -
        -
        -
        - -
        -
        -
        - -
        -
        - - 刷新 - 添加 -
        - - -
        -
        - - -
        -
        -
        -
        -
        -{/block} -{block name="script"} - - - - - +{extend name="common/layout" /} +{block name="title"}定时切换策略{/block} +{block name="main"} + +
        +
        +
        +
        + +
        +
        + +
        + +
        +
        +
        + +
        +
        +
        + +
        +
        + + 刷新 + 添加 +
        + + +
        +
        + + +
        +
        +
        +
        +
        +{/block} +{block name="script"} + + + + + {/block} \ No newline at end of file diff --git a/app/view/schedule/staskform.html b/app/view/schedule/staskform.html index 842836a..7299f21 100644 --- a/app/view/schedule/staskform.html +++ b/app/view/schedule/staskform.html @@ -1,269 +1,269 @@ -{extend name="common/layout" /} -{block name="title"}定时切换策略{/block} -{block name="main"} - -
        -
        -
        -

        返回{if $action=='edit'}编辑{else}添加{/if}定时切换策略

        -
        -
        -
        - -
        -
        - - . - -
        -
        -
        -
        - -
        - -
        - -
        -
        -
        -
        - -
        - - -
        -
        -
        - -
        - -
        -
        -
        - -
        -
        - - - - - - -
        -
        -
        -
        - -
        - - - - -
        -
        -
        - -
        - -
        -
        -
        - -
        - - - -
        -
        -
        - -
        - -
        -
        -
        -
        -
        -
        -
        - -
        -{/block} -{block name="script"} - - - - +{extend name="common/layout" /} +{block name="title"}定时切换策略{/block} +{block name="main"} + +
        +
        +
        +

        返回{if $action=='edit'}编辑{else}添加{/if}定时切换策略

        +
        +
        +
        + +
        +
        + + . + +
        +
        +
        +
        + +
        + +
        + +
        +
        +
        +
        + +
        + + +
        +
        +
        + +
        + +
        +
        +
        + +
        +
        + + + + + + +
        +
        +
        +
        + +
        + + + + +
        +
        +
        + +
        + +
        +
        +
        + +
        + + + +
        +
        +
        + +
        + +
        +
        +
        +
        +
        +
        +
        + +
        +{/block} +{block name="script"} + + + + {/block} \ No newline at end of file diff --git a/app/view/system/cronset.html b/app/view/system/cronset.html index 8bf5564..153d072 100644 --- a/app/view/system/cronset.html +++ b/app/view/system/cronset.html @@ -80,7 +80,7 @@
        {/block} {block name="script"} - + + - +{extend name="common/layout" /} +{block name="title"}通知设置{/block} +{block name="main"} +
        +
        +
        +

        发信邮箱设置

        +
        +
        +
        + +
        +
        +
        +
        + +
        +
        +
        + +
        +
        +
        + +
        +
        +
        + +
        +
        +
        +
        +
        + +
        +
        +
        + +
        +
        +
        + +
        +
        +
        +
        + +
        +
        +
        + +
        +
        +
        + +
        +
        +

        微信公众号消息接口设置

        +
        +
        +
        + +
        +
        +
        + +
        +
        +
        +
        +
        +
        +
        + +
        +
        +

        Telegram机器人接口设置

        +
        +
        +
        + +
        +
        +
        + +
        +
        +
        + +
        +
        + +
        + +
        +
        +
        + +
        +
        +

        群机器人Webhook

        +
        +
        +
        + +
        +
        +
        + +
        +
        +
        + +
        +
        +
        +{/block} +{block name="script"} + + {/block} \ No newline at end of file diff --git a/app/view/system/proxyset.html b/app/view/system/proxyset.html index 7e93786..d9c61ed 100644 --- a/app/view/system/proxyset.html +++ b/app/view/system/proxyset.html @@ -1,114 +1,114 @@ -{extend name="common/layout" /} -{block name="title"}代理设置{/block} -{block name="main"} -
        -
        -
        -

        代理服务器设置

        -
        -
        -
        - -
        -

        -
        - -
        -

        -
        - -
        -

        -
        - -
        -

        -
        - -
        -

        - -
        -
        - -
        -
        -
        -{/block} -{block name="script"} - - +{extend name="common/layout" /} +{block name="title"}代理设置{/block} +{block name="main"} +
        +
        +
        +

        代理服务器设置

        +
        +
        +
        + +
        +

        +
        + +
        +

        +
        + +
        +

        +
        + +
        +

        +
        + +
        +

        + +
        +
        + +
        +
        +
        +{/block} +{block name="script"} + + {/block} \ No newline at end of file diff --git a/app/view/user/log.html b/app/view/user/log.html index bcf4547..4cf9886 100644 --- a/app/view/user/log.html +++ b/app/view/user/log.html @@ -1,77 +1,77 @@ -{extend name="common/layout" /} -{block name="title"}操作日志{/block} -{block name="main"} - -
        -
        -
        -
        - -
        -
        - - {if request()->user['level'] eq 2}{/if} - - -
        - - 刷新 -
        - - -
        -
        -
        -
        -
        -{/block} -{block name="script"} - - - - - +{extend name="common/layout" /} +{block name="title"}操作日志{/block} +{block name="main"} + +
        +
        +
        +
        + +
        +
        + + {if request()->user['level'] eq 2}{/if} + + +
        + + 刷新 +
        + + +
        +
        +
        +
        +
        +{/block} +{block name="script"} + + + + + {/block} \ No newline at end of file diff --git a/app/view/user/user.html b/app/view/user/user.html index 57b8f72..b92cf99 100644 --- a/app/view/user/user.html +++ b/app/view/user/user.html @@ -1,338 +1,338 @@ -{extend name="common/layout" /} -{block name="title"}用户管理{/block} -{block name="main"} - - -
        -
        -
        -
        - -
        -
        - - -
        - - 刷新 - 添加 -
        - - -
        -
        -
        -
        -
        -{/block} -{block name="script"} - - - - - - - +{extend name="common/layout" /} +{block name="title"}用户管理{/block} +{block name="main"} + + +
        +
        +
        +
        + +
        +
        + + +
        + + 刷新 + 添加 +
        + + +
        +
        +
        +
        +
        +{/block} +{block name="script"} + + + + + + + {/block} \ No newline at end of file diff --git a/composer.json b/composer.json index fa908d2..cc6e73a 100644 --- a/composer.json +++ b/composer.json @@ -1,90 +1,90 @@ -{ - "$schema": "https://getcomposer.org/schema.json", - "name": "netcccyun/dnsmgr", - "description": "聚合DNS管理系统", - "type": "project", - "keywords": [ - "thinkphp", - "dns", - "dnsmanager", - "cccyun" - ], - "homepage": "https://blog.cccyun.cn/post-526.html", - "license": "Apache-2.0", - "authors": [ - { - "name": "liu21st", - "email": "liu21st@gmail.com", - "role": "Framework Developer" - }, - { - "name": "yunwuxin", - "email": "448901948@qq.com", - "role": "Framework Developer" - }, - { - "name": "netcccyun", - "homepage": "https://blog.cccyun.cn", - "role": "Project Owner" - }, - { - "name": "coolxitech", - "email": "admin@kuxi.tech", - "homepage": "https://www.kuxi.tech", - "role": "Project Developer" - }, - { - "name": "耗子", - "email": "haozi@loli.email", - "homepage": "https://hzbk.net", - "role": "Project Developer" - } - ], - "require": { - "php": ">=8.2.0", - "ext-curl": "*", - "ext-ftp": "*", - "ext-gd": "*", - "ext-mbstring": "*", - "ext-openssl": "*", - "ext-pdo": "*", - "ext-sockets": "*", - "ext-ssh2": "*", - "cccyun/php-whois": "^1.0", - "cccyun/think-captcha": "^3.0", - "guzzlehttp/guzzle": "^7.0", - "phpmailer/phpmailer": "^6.10", - "symfony/polyfill-intl-idn": "^1.32", - "symfony/polyfill-mbstring": "^1.32", - "symfony/polyfill-php81": "^1.32", - "symfony/polyfill-php82": "^1.32", - "symfony/yaml": "^7.3", - "topthink/framework": "^8.1.0", - "topthink/think-orm": "^4.0", - "topthink/think-view": "^2.0" - }, - "require-dev": { - "symfony/var-dumper": "^7.3", - "topthink/think-trace":"^2.0", - "swoole/ide-helper": "^6.0" - }, - "autoload": { - "psr-4": { - "app\\": "app" - } - }, - "config": { - "optimize-autoloader": true, - "sort-packages": true, - "platform-check": false, - "preferred-install": "dist" - }, - "scripts": { - "post-autoload-dump": [ - "@php think service:discover", - "@php think vendor:publish" - ] - }, - "minimum-stability": "stable", - "prefer-stable": true -} +{ + "$schema": "https://getcomposer.org/schema.json", + "name": "netcccyun/dnsmgr", + "description": "聚合DNS管理系统", + "type": "project", + "keywords": [ + "thinkphp", + "dns", + "dnsmanager", + "cccyun" + ], + "homepage": "https://blog.cccyun.cn/post-526.html", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com", + "role": "Framework Developer" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com", + "role": "Framework Developer" + }, + { + "name": "netcccyun", + "homepage": "https://blog.cccyun.cn", + "role": "Project Owner" + }, + { + "name": "coolxitech", + "email": "admin@kuxi.tech", + "homepage": "https://www.kuxi.tech", + "role": "Project Developer" + }, + { + "name": "耗子", + "email": "haozi@loli.email", + "homepage": "https://hzbk.net", + "role": "Project Developer" + } + ], + "require": { + "php": ">=8.2.0", + "ext-curl": "*", + "ext-ftp": "*", + "ext-gd": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-pdo": "*", + "ext-sockets": "*", + "ext-ssh2": "*", + "cccyun/php-whois": "^1.0", + "cccyun/think-captcha": "^3.0", + "guzzlehttp/guzzle": "^7.0", + "phpmailer/phpmailer": "^6.10", + "symfony/polyfill-intl-idn": "^1.32", + "symfony/polyfill-mbstring": "^1.32", + "symfony/polyfill-php81": "^1.32", + "symfony/polyfill-php82": "^1.32", + "symfony/yaml": "^7.3", + "topthink/framework": "^8.1.0", + "topthink/think-orm": "^4.0", + "topthink/think-view": "^2.0" + }, + "require-dev": { + "symfony/var-dumper": "^7.3", + "topthink/think-trace":"^2.0", + "swoole/ide-helper": "^6.0" + }, + "autoload": { + "psr-4": { + "app\\": "app" + } + }, + "config": { + "optimize-autoloader": true, + "sort-packages": true, + "platform-check": false, + "preferred-install": "dist" + }, + "scripts": { + "post-autoload-dump": [ + "@php think service:discover", + "@php think vendor:publish" + ] + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/config/app.php b/config/app.php index 5162d40..646b392 100644 --- a/config/app.php +++ b/config/app.php @@ -1,37 +1,37 @@ - env('app.host', ''), - // 应用的命名空间 - 'app_namespace' => '', - // 是否启用路由 - 'with_route' => true, - // 默认应用 - 'default_app' => 'index', - // 默认时区 - 'default_timezone' => 'Asia/Shanghai', - - // 应用映射(自动多应用模式有效) - 'app_map' => [], - // 域名绑定(自动多应用模式有效) - 'domain_bind' => [], - // 禁止URL访问的应用列表(自动多应用模式有效) - 'deny_app_list' => [], - - // 异常页面的模板文件 - 'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl', - - // 错误显示信息,非调试模式有效 - 'error_message' => '页面错误!请稍后再试~', - // 显示错误信息 - 'show_error_msg' => true, - 'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl', - - 'version' => '1041', - - 'dbversion' => '1040' -]; + env('app.host', ''), + // 应用的命名空间 + 'app_namespace' => '', + // 是否启用路由 + 'with_route' => true, + // 默认应用 + 'default_app' => 'index', + // 默认时区 + 'default_timezone' => 'Asia/Shanghai', + + // 应用映射(自动多应用模式有效) + 'app_map' => [], + // 域名绑定(自动多应用模式有效) + 'domain_bind' => [], + // 禁止URL访问的应用列表(自动多应用模式有效) + 'deny_app_list' => [], + + // 异常页面的模板文件 + 'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl', + + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => true, + 'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl', + + 'version' => '1041', + + 'dbversion' => '1040' +]; diff --git a/public/static/css/bootstrap-3.4.1.min.css b/public/static/css/bootstrap-3.4.1.min.css new file mode 100644 index 0000000..5b96335 --- /dev/null +++ b/public/static/css/bootstrap-3.4.1.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:"Glyphicons Halflings";src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../fonts/glyphicons-halflings-regular.woff2) format("woff2"),url(../fonts/glyphicons-halflings-regular.woff) format("woff"),url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*=col-]{padding-right:0;padding-left:0}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm input[type=time],input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg input[type=time],input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;background-image:none;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;background-image:none;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;filter:alpha(opacity=90);opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/public/static/css/bootstrap-datetimepicker-4.17.47.min.css b/public/static/css/bootstrap-datetimepicker-4.17.47.min.css new file mode 100644 index 0000000..5950ad2 --- /dev/null +++ b/public/static/css/bootstrap-datetimepicker-4.17.47.min.css @@ -0,0 +1,5 @@ +/*! + * Datetimepicker for Bootstrap 3 + * version : 4.17.47 + * https://github.com/Eonasdan/bootstrap-datetimepicker/ + */.bootstrap-datetimepicker-widget{list-style:none}.bootstrap-datetimepicker-widget.dropdown-menu{display:block;margin:2px 0;padding:4px;width:19em}@media (min-width:768px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}@media (min-width:992px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}@media (min-width:1200px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}.bootstrap-datetimepicker-widget.dropdown-menu:before,.bootstrap-datetimepicker-widget.dropdown-menu:after{content:'';display:inline-block;position:absolute}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before{border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,0.2);top:-7px;left:7px}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid white;top:-6px;left:8px}.bootstrap-datetimepicker-widget.dropdown-menu.top:before{border-left:7px solid transparent;border-right:7px solid transparent;border-top:7px solid #ccc;border-top-color:rgba(0,0,0,0.2);bottom:-7px;left:6px}.bootstrap-datetimepicker-widget.dropdown-menu.top:after{border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid white;bottom:-6px;left:7px}.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after{left:auto;right:7px}.bootstrap-datetimepicker-widget .list-unstyled{margin:0}.bootstrap-datetimepicker-widget a[data-action]{padding:6px 0}.bootstrap-datetimepicker-widget a[data-action]:active{box-shadow:none}.bootstrap-datetimepicker-widget .timepicker-hour,.bootstrap-datetimepicker-widget .timepicker-minute,.bootstrap-datetimepicker-widget .timepicker-second{width:54px;font-weight:bold;font-size:1.2em;margin:0}.bootstrap-datetimepicker-widget button[data-action]{padding:6px}.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Increment Hours"}.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Increment Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Decrement Hours"}.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Decrement Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Show Hours"}.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Show Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Toggle AM/PM"}.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Clear the picker"}.bootstrap-datetimepicker-widget .btn[data-action="today"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Set the date to today"}.bootstrap-datetimepicker-widget .picker-switch{text-align:center}.bootstrap-datetimepicker-widget .picker-switch::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Toggle Date and Time Screens"}.bootstrap-datetimepicker-widget .picker-switch td{padding:0;margin:0;height:auto;width:auto;line-height:inherit}.bootstrap-datetimepicker-widget .picker-switch td span{line-height:2.5;height:2.5em;width:100%}.bootstrap-datetimepicker-widget table{width:100%;margin:0}.bootstrap-datetimepicker-widget table td,.bootstrap-datetimepicker-widget table th{text-align:center;border-radius:4px}.bootstrap-datetimepicker-widget table th{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget table th.picker-switch{width:145px}.bootstrap-datetimepicker-widget table th.disabled,.bootstrap-datetimepicker-widget table th.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget table th.prev::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Previous Month"}.bootstrap-datetimepicker-widget table th.next::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Next Month"}.bootstrap-datetimepicker-widget table thead tr:first-child th{cursor:pointer}.bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#eee}.bootstrap-datetimepicker-widget table td{height:54px;line-height:54px;width:54px}.bootstrap-datetimepicker-widget table td.cw{font-size:.8em;height:20px;line-height:20px;color:#777}.bootstrap-datetimepicker-widget table td.day{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget table td.day:hover,.bootstrap-datetimepicker-widget table td.hour:hover,.bootstrap-datetimepicker-widget table td.minute:hover,.bootstrap-datetimepicker-widget table td.second:hover{background:#eee;cursor:pointer}.bootstrap-datetimepicker-widget table td.old,.bootstrap-datetimepicker-widget table td.new{color:#777}.bootstrap-datetimepicker-widget table td.today{position:relative}.bootstrap-datetimepicker-widget table td.today:before{content:'';display:inline-block;border:solid transparent;border-width:0 0 7px 7px;border-bottom-color:#337ab7;border-top-color:rgba(0,0,0,0.2);position:absolute;bottom:4px;right:4px}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{background-color:#337ab7;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget table td.active.today:before{border-bottom-color:#fff}.bootstrap-datetimepicker-widget table td.disabled,.bootstrap-datetimepicker-widget table td.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget table td span{display:inline-block;width:54px;height:54px;line-height:54px;margin:2px 1.5px;cursor:pointer;border-radius:4px}.bootstrap-datetimepicker-widget table td span:hover{background:#eee}.bootstrap-datetimepicker-widget table td span.active{background-color:#337ab7;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget table td span.old{color:#777}.bootstrap-datetimepicker-widget table td span.disabled,.bootstrap-datetimepicker-widget table td span.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget.usetwentyfour td.hour{height:27px;line-height:27px}.bootstrap-datetimepicker-widget.wider{width:21em}.bootstrap-datetimepicker-widget .datepicker-decades .decade{line-height:1.8em !important}.input-group.date .input-group-addon{cursor:pointer}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0} \ No newline at end of file diff --git a/public/static/css/bootstrap-table.css b/public/static/css/bootstrap-table.css index 607d063..5851cc1 100644 --- a/public/static/css/bootstrap-table.css +++ b/public/static/css/bootstrap-table.css @@ -1,13 +1,13 @@ -.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .columns,.bootstrap-table .fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.4286}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0!important}.bootstrap-table .fixed-table-container .table td,.bootstrap-table .fixed-table-container .table th{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus{outline:0 solid transparent}.bootstrap-table .fixed-table-container .table thead th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px!important}.bootstrap-table .fixed-table-container .table thead th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==")}.bootstrap-table .fixed-table-container .table thead th .desc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ")}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:700;display:inline-block;min-width:30%;width:auto!important;text-align:left!important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100%!important;text-align:left!important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio]{margin:0 auto!important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.3rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;max-width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:loading;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination,.bootstrap-table .fixed-table-pagination>.pagination-detail{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:"\2B05"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:"\27A1"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#fff;height:calc(100vh);overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes loading{0%{opacity:0}50%{opacity:1}to{opacity:0}} -.table-bottom-border{border-bottom: 2px solid #ddd;} -@media screen and (max-width:767px){.fixed-table-body{border:1px solid #ddd}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{white-space:nowrap}.columns-right{display:none;}} -.bootstrap-table.bootstrap3 .fixed-table-pagination>.pagination .page-jump-to{display:inline-block}.bootstrap-table.bootstrap3 .fixed-table-pagination>.pagination .input-group-btn{width:auto}.bootstrap-table .fixed-table-pagination>.pagination .page-jump-to input{width:70px;margin-left:5px;text-align:center;float:left} -.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle} -.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle} -.bv-form .help-block{margin-bottom:0}.bv-form .tooltip-inner{text-align:left}.nav-tabs li.bv-tab-success>a{color:#3c763d}.nav-tabs li.bv-tab-error>a{color:#a94442}.bv-form .bv-icon-no-label{top:0}.bv-form .bv-icon-input-group{top:0;z-index:100} -.material-switch>input[type=checkbox]{display:none} -.material-switch>label{cursor:pointer;height:0;position:relative;width:36px;margin-left:4px} -.material-switch>label::before{background:#000;box-shadow:inset 0 0 10px rgba(0,0,0,.5);border-radius:8px;content:'';height:16px;margin-top:-7px;position:absolute;opacity:.3;transition:all .4s ease-in-out;width:36px} -.material-switch>label::after{background:#fff;border-radius:16px;box-shadow:0 0 5px rgba(0,0,0,.3);content:'';height:20px;left:-2px;margin-top:-5px;position:absolute;top:-4px;transition:all .3s ease-in-out;width:20px} -.material-switch>input[type=checkbox]:checked+label::before{background:inherit;opacity:.5} -.material-switch>input[type=checkbox]:checked+label::after{background:inherit;left:20px} +.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .columns,.bootstrap-table .fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.4286}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0!important}.bootstrap-table .fixed-table-container .table td,.bootstrap-table .fixed-table-container .table th{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus{outline:0 solid transparent}.bootstrap-table .fixed-table-container .table thead th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px!important}.bootstrap-table .fixed-table-container .table thead th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==")}.bootstrap-table .fixed-table-container .table thead th .desc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ")}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:700;display:inline-block;min-width:30%;width:auto!important;text-align:left!important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100%!important;text-align:left!important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio]{margin:0 auto!important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.3rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;max-width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:loading;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination,.bootstrap-table .fixed-table-pagination>.pagination-detail{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:"\2B05"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:"\27A1"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#fff;height:calc(100vh);overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes loading{0%{opacity:0}50%{opacity:1}to{opacity:0}} +.table-bottom-border{border-bottom: 2px solid #ddd;} +@media screen and (max-width:767px){.fixed-table-body{border:1px solid #ddd}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{white-space:nowrap}.columns-right{display:none;}} +.bootstrap-table.bootstrap3 .fixed-table-pagination>.pagination .page-jump-to{display:inline-block}.bootstrap-table.bootstrap3 .fixed-table-pagination>.pagination .input-group-btn{width:auto}.bootstrap-table .fixed-table-pagination>.pagination .page-jump-to input{width:70px;margin-left:5px;text-align:center;float:left} +.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle} +.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle} +.bv-form .help-block{margin-bottom:0}.bv-form .tooltip-inner{text-align:left}.nav-tabs li.bv-tab-success>a{color:#3c763d}.nav-tabs li.bv-tab-error>a{color:#a94442}.bv-form .bv-icon-no-label{top:0}.bv-form .bv-icon-input-group{top:0;z-index:100} +.material-switch>input[type=checkbox]{display:none} +.material-switch>label{cursor:pointer;height:0;position:relative;width:36px;margin-left:4px} +.material-switch>label::before{background:#000;box-shadow:inset 0 0 10px rgba(0,0,0,.5);border-radius:8px;content:'';height:16px;margin-top:-7px;position:absolute;opacity:.3;transition:all .4s ease-in-out;width:36px} +.material-switch>label::after{background:#fff;border-radius:16px;box-shadow:0 0 5px rgba(0,0,0,.3);content:'';height:20px;left:-2px;margin-top:-5px;position:absolute;top:-4px;transition:all .3s ease-in-out;width:20px} +.material-switch>input[type=checkbox]:checked+label::before{background:inherit;opacity:.5} +.material-switch>input[type=checkbox]:checked+label::after{background:inherit;left:20px} diff --git a/public/static/css/font-awesome-4.7.0.min.css b/public/static/css/font-awesome-4.7.0.min.css new file mode 100644 index 0000000..540440c --- /dev/null +++ b/public/static/css/font-awesome-4.7.0.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/public/static/css/select2-4.0.13.min.css b/public/static/css/select2-4.0.13.min.css new file mode 100644 index 0000000..7c18ad5 --- /dev/null +++ b/public/static/css/select2-4.0.13.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/public/static/fonts/fontawesome-webfont.eot b/public/static/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/public/static/fonts/fontawesome-webfont.eot differ diff --git a/public/static/fonts/fontawesome-webfont.svg b/public/static/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/public/static/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/fonts/fontawesome-webfont.ttf b/public/static/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/public/static/fonts/fontawesome-webfont.ttf differ diff --git a/public/static/fonts/fontawesome-webfont.woff b/public/static/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/public/static/fonts/fontawesome-webfont.woff differ diff --git a/public/static/fonts/fontawesome-webfont.woff2 b/public/static/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/public/static/fonts/fontawesome-webfont.woff2 differ diff --git a/public/static/fonts/glyphicons-halflings-regular.eot b/public/static/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000..b93a495 Binary files /dev/null and b/public/static/fonts/glyphicons-halflings-regular.eot differ diff --git a/public/static/fonts/glyphicons-halflings-regular.svg b/public/static/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 0000000..94fb549 --- /dev/null +++ b/public/static/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/static/fonts/glyphicons-halflings-regular.ttf b/public/static/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000..1413fc6 Binary files /dev/null and b/public/static/fonts/glyphicons-halflings-regular.ttf differ diff --git a/public/static/fonts/glyphicons-halflings-regular.woff b/public/static/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 0000000..9e61285 Binary files /dev/null and b/public/static/fonts/glyphicons-halflings-regular.woff differ diff --git a/public/static/fonts/glyphicons-halflings-regular.woff2 b/public/static/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 0000000..64539b5 Binary files /dev/null and b/public/static/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/public/static/js/FileSaver-2.0.5.min.js b/public/static/js/FileSaver-2.0.5.min.js new file mode 100644 index 0000000..77f4ff9 --- /dev/null +++ b/public/static/js/FileSaver-2.0.5.min.js @@ -0,0 +1,3 @@ +(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.navigator&&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)}); + +//# sourceMappingURL=FileSaver.min.js.map \ No newline at end of file diff --git a/public/static/js/bootstrap-3.4.1.min.js b/public/static/js/bootstrap-3.4.1.min.js new file mode 100644 index 0000000..eb0a8b4 --- /dev/null +++ b/public/static/js/bootstrap-3.4.1.min.js @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under the MIT license + */ +if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");!function(t){"use strict";var e=jQuery.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||3this.$items.length-1||t<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){e.to(t)}):i==t?this.pause().cycle():this.slide(idocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&t?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!t?this.scrollbarWidth:""})},s.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},s.prototype.checkScrollbar=function(){var t=window.innerWidth;if(!t){var e=document.documentElement.getBoundingClientRect();t=e.right-Math.abs(e.left)}this.bodyIsOverflowing=document.body.clientWidth
        ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0},sanitize:!0,sanitizeFn:null,whiteList:t},m.prototype.init=function(t,e,i){if(this.enabled=!0,this.type=t,this.$element=g(e),this.options=this.getOptions(i),this.$viewport=this.options.viewport&&g(document).find(g.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var o=this.options.trigger.split(" "),n=o.length;n--;){var s=o[n];if("click"==s)this.$element.on("click."+this.type,this.options.selector,g.proxy(this.toggle,this));else if("manual"!=s){var a="hover"==s?"mouseenter":"focusin",r="hover"==s?"mouseleave":"focusout";this.$element.on(a+"."+this.type,this.options.selector,g.proxy(this.enter,this)),this.$element.on(r+"."+this.type,this.options.selector,g.proxy(this.leave,this))}}this.options.selector?this._options=g.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},m.prototype.getDefaults=function(){return m.DEFAULTS},m.prototype.getOptions=function(t){var e=this.$element.data();for(var i in e)e.hasOwnProperty(i)&&-1!==g.inArray(i,o)&&delete e[i];return(t=g.extend({},this.getDefaults(),e,t)).delay&&"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),t.sanitize&&(t.template=n(t.template,t.whiteList,t.sanitizeFn)),t},m.prototype.getDelegateOptions=function(){var i={},o=this.getDefaults();return this._options&&g.each(this._options,function(t,e){o[t]!=e&&(i[t]=e)}),i},m.prototype.enter=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusin"==t.type?"focus":"hover"]=!0),e.tip().hasClass("in")||"in"==e.hoverState)e.hoverState="in";else{if(clearTimeout(e.timeout),e.hoverState="in",!e.options.delay||!e.options.delay.show)return e.show();e.timeout=setTimeout(function(){"in"==e.hoverState&&e.show()},e.options.delay.show)}},m.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},m.prototype.leave=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusout"==t.type?"focus":"hover"]=!1),!e.isInStateTrue()){if(clearTimeout(e.timeout),e.hoverState="out",!e.options.delay||!e.options.delay.hide)return e.hide();e.timeout=setTimeout(function(){"out"==e.hoverState&&e.hide()},e.options.delay.hide)}},m.prototype.show=function(){var t=g.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(t);var e=g.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(t.isDefaultPrevented()||!e)return;var i=this,o=this.tip(),n=this.getUID(this.type);this.setContent(),o.attr("id",n),this.$element.attr("aria-describedby",n),this.options.animation&&o.addClass("fade");var s="function"==typeof this.options.placement?this.options.placement.call(this,o[0],this.$element[0]):this.options.placement,a=/\s?auto?\s?/i,r=a.test(s);r&&(s=s.replace(a,"")||"top"),o.detach().css({top:0,left:0,display:"block"}).addClass(s).data("bs."+this.type,this),this.options.container?o.appendTo(g(document).find(this.options.container)):o.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var l=this.getPosition(),h=o[0].offsetWidth,d=o[0].offsetHeight;if(r){var p=s,c=this.getPosition(this.$viewport);s="bottom"==s&&l.bottom+d>c.bottom?"top":"top"==s&&l.top-dc.width?"left":"left"==s&&l.left-ha.top+a.height&&(n.top=a.top+a.height-l)}else{var h=e.left-s,d=e.left+s+i;ha.right&&(n.left=a.left+a.width-d)}return n},m.prototype.getTitle=function(){var t=this.$element,e=this.options;return t.attr("data-original-title")||("function"==typeof e.title?e.title.call(t[0]):e.title)},m.prototype.getUID=function(t){for(;t+=~~(1e6*Math.random()),document.getElementById(t););return t},m.prototype.tip=function(){if(!this.$tip&&(this.$tip=g(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},m.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},m.prototype.enable=function(){this.enabled=!0},m.prototype.disable=function(){this.enabled=!1},m.prototype.toggleEnabled=function(){this.enabled=!this.enabled},m.prototype.toggle=function(t){var e=this;t&&((e=g(t.currentTarget).data("bs."+this.type))||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e))),t?(e.inState.click=!e.inState.click,e.isInStateTrue()?e.enter(e):e.leave(e)):e.tip().hasClass("in")?e.leave(e):e.enter(e)},m.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null,t.$element=null})},m.prototype.sanitizeHtml=function(t){return n(t,this.options.whiteList,this.options.sanitizeFn)};var e=g.fn.tooltip;g.fn.tooltip=function i(o){return this.each(function(){var t=g(this),e=t.data("bs.tooltip"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.tooltip",e=new m(this,i)),"string"==typeof o&&e[o]())})},g.fn.tooltip.Constructor=m,g.fn.tooltip.noConflict=function(){return g.fn.tooltip=e,this}}(jQuery),function(n){"use strict";var s=function(t,e){this.init("popover",t,e)};if(!n.fn.tooltip)throw new Error("Popover requires tooltip.js");s.VERSION="3.4.1",s.DEFAULTS=n.extend({},n.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),((s.prototype=n.extend({},n.fn.tooltip.Constructor.prototype)).constructor=s).prototype.getDefaults=function(){return s.DEFAULTS},s.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),i=this.getContent();if(this.options.html){var o=typeof i;this.options.sanitize&&(e=this.sanitizeHtml(e),"string"===o&&(i=this.sanitizeHtml(i))),t.find(".popover-title").html(e),t.find(".popover-content").children().detach().end()["string"===o?"html":"append"](i)}else t.find(".popover-title").text(e),t.find(".popover-content").children().detach().end().text(i);t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},s.prototype.hasContent=function(){return this.getTitle()||this.getContent()},s.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},s.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var t=n.fn.popover;n.fn.popover=function e(o){return this.each(function(){var t=n(this),e=t.data("bs.popover"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.popover",e=new s(this,i)),"string"==typeof o&&e[o]())})},n.fn.popover.Constructor=s,n.fn.popover.noConflict=function(){return n.fn.popover=t,this}}(jQuery),function(s){"use strict";function n(t,e){this.$body=s(document.body),this.$scrollElement=s(t).is(document.body)?s(window):s(t),this.options=s.extend({},n.DEFAULTS,e),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",s.proxy(this.process,this)),this.refresh(),this.process()}function e(o){return this.each(function(){var t=s(this),e=t.data("bs.scrollspy"),i="object"==typeof o&&o;e||t.data("bs.scrollspy",e=new n(this,i)),"string"==typeof o&&e[o]()})}n.VERSION="3.4.1",n.DEFAULTS={offset:10},n.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},n.prototype.refresh=function(){var t=this,o="offset",n=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),s.isWindow(this.$scrollElement[0])||(o="position",n=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var t=s(this),e=t.data("target")||t.attr("href"),i=/^#./.test(e)&&s(e);return i&&i.length&&i.is(":visible")&&[[i[o]().top+n,e]]||null}).sort(function(t,e){return t[0]-e[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},n.prototype.process=function(){var t,e=this.$scrollElement.scrollTop()+this.options.offset,i=this.getScrollHeight(),o=this.options.offset+i-this.$scrollElement.height(),n=this.offsets,s=this.targets,a=this.activeTarget;if(this.scrollHeight!=i&&this.refresh(),o<=e)return a!=(t=s[s.length-1])&&this.activate(t);if(a&&e=n[t]&&(n[t+1]===undefined||e .active"),n=i&&r.support.transition&&(o.length&&o.hasClass("fade")||!!e.find("> .fade").length);function s(){o.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),t.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),n?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu").length&&t.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),i&&i()}o.length&&n?o.one("bsTransitionEnd",s).emulateTransitionEnd(a.TRANSITION_DURATION):s(),o.removeClass("in")};var t=r.fn.tab;r.fn.tab=e,r.fn.tab.Constructor=a,r.fn.tab.noConflict=function(){return r.fn.tab=t,this};var i=function(t){t.preventDefault(),e.call(r(this),"show")};r(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',i).on("click.bs.tab.data-api",'[data-toggle="pill"]',i)}(jQuery),function(l){"use strict";var h=function(t,e){this.options=l.extend({},h.DEFAULTS,e);var i=this.options.target===h.DEFAULTS.target?l(this.options.target):l(document).find(this.options.target);this.$target=i.on("scroll.bs.affix.data-api",l.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",l.proxy(this.checkPositionWithEventLoop,this)),this.$element=l(t),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};function i(o){return this.each(function(){var t=l(this),e=t.data("bs.affix"),i="object"==typeof o&&o;e||t.data("bs.affix",e=new h(this,i)),"string"==typeof o&&e[o]()})}h.VERSION="3.4.1",h.RESET="affix affix-top affix-bottom",h.DEFAULTS={offset:0,target:window},h.prototype.getState=function(t,e,i,o){var n=this.$target.scrollTop(),s=this.$element.offset(),a=this.$target.height();if(null!=i&&"top"==this.affixed)return n1)throw new TypeError("isEnabled expects a single character string parameter");switch(a){case"y":return i.indexOf("Y")!==-1;case"M":return i.indexOf("M")!==-1;case"d":return i.toLowerCase().indexOf("d")!==-1;case"h":case"H":return i.toLowerCase().indexOf("h")!==-1;case"m":return i.indexOf("m")!==-1;case"s":return i.indexOf("s")!==-1;default:return!1}},A=function(){return z("h")||z("m")||z("s")},B=function(){return z("y")||z("M")||z("d")},C=function(){var b=a("").append(a("").append(a("").addClass("prev").attr("data-action","previous").append(a("").addClass(d.icons.previous))).append(a("").addClass("picker-switch").attr("data-action","pickerSwitch").attr("colspan",d.calendarWeeks?"6":"5")).append(a("").addClass("next").attr("data-action","next").append(a("").addClass(d.icons.next)))),c=a("").append(a("").append(a("").attr("colspan",d.calendarWeeks?"8":"7")));return[a("
        ").addClass("datepicker-days").append(a("").addClass("table-condensed").append(b).append(a(""))),a("
        ").addClass("datepicker-months").append(a("
        ").addClass("table-condensed").append(b.clone()).append(c.clone())),a("
        ").addClass("datepicker-years").append(a("
        ").addClass("table-condensed").append(b.clone()).append(c.clone())),a("
        ").addClass("datepicker-decades").append(a("
        ").addClass("table-condensed").append(b.clone()).append(c.clone()))]},D=function(){var b=a(""),c=a(""),e=a("");return z("h")&&(b.append(a("
        ").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementHour}).addClass("btn").attr("data-action","incrementHours").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-hour").attr({"data-time-component":"hours",title:d.tooltips.pickHour}).attr("data-action","showHours"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementHour}).addClass("btn").attr("data-action","decrementHours").append(a("").addClass(d.icons.down))))),z("m")&&(z("h")&&(b.append(a("").addClass("separator")),c.append(a("").addClass("separator").html(":")),e.append(a("").addClass("separator"))),b.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementMinute}).addClass("btn").attr("data-action","incrementMinutes").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-minute").attr({"data-time-component":"minutes",title:d.tooltips.pickMinute}).attr("data-action","showMinutes"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementMinute}).addClass("btn").attr("data-action","decrementMinutes").append(a("").addClass(d.icons.down))))),z("s")&&(z("m")&&(b.append(a("").addClass("separator")),c.append(a("").addClass("separator").html(":")),e.append(a("").addClass("separator"))),b.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.incrementSecond}).addClass("btn").attr("data-action","incrementSeconds").append(a("").addClass(d.icons.up)))),c.append(a("").append(a("").addClass("timepicker-second").attr({"data-time-component":"seconds",title:d.tooltips.pickSecond}).attr("data-action","showSeconds"))),e.append(a("").append(a("").attr({href:"#",tabindex:"-1",title:d.tooltips.decrementSecond}).addClass("btn").attr("data-action","decrementSeconds").append(a("").addClass(d.icons.down))))),h||(b.append(a("").addClass("separator")),c.append(a("").append(a("").addClass("separator"))),a("
        ").addClass("timepicker-picker").append(a("").addClass("table-condensed").append([b,c,e]))},E=function(){var b=a("
        ").addClass("timepicker-hours").append(a("
        ").addClass("table-condensed")),c=a("
        ").addClass("timepicker-minutes").append(a("
        ").addClass("table-condensed")),d=a("
        ").addClass("timepicker-seconds").append(a("
        ").addClass("table-condensed")),e=[D()];return z("h")&&e.push(b),z("m")&&e.push(c),z("s")&&e.push(d),e},F=function(){var b=[];return d.showTodayButton&&b.push(a("
        ").append(a("").attr({"data-action":"today",title:d.tooltips.today}).append(a("").addClass(d.icons.today)))),!d.sideBySide&&B()&&A()&&b.push(a("").append(a("").attr({"data-action":"togglePicker",title:d.tooltips.selectTime}).append(a("").addClass(d.icons.time)))),d.showClear&&b.push(a("").append(a("").attr({"data-action":"clear",title:d.tooltips.clear}).append(a("").addClass(d.icons.clear)))),d.showClose&&b.push(a("").append(a("").attr({"data-action":"close",title:d.tooltips.close}).append(a("").addClass(d.icons.close)))),a("").addClass("table-condensed").append(a("").append(a("").append(b)))},G=function(){var b=a("
        ").addClass("bootstrap-datetimepicker-widget dropdown-menu"),c=a("
        ").addClass("datepicker").append(C()),e=a("
        ").addClass("timepicker").append(E()),f=a("
          ").addClass("list-unstyled"),g=a("
        • ").addClass("picker-switch"+(d.collapse?" accordion-toggle":"")).append(F());return d.inline&&b.removeClass("dropdown-menu"),h&&b.addClass("usetwentyfour"),z("s")&&!h&&b.addClass("wider"),d.sideBySide&&B()&&A()?(b.addClass("timepicker-sbs"),"top"===d.toolbarPlacement&&b.append(g),b.append(a("
          ").addClass("row").append(c.addClass("col-md-6")).append(e.addClass("col-md-6"))),"bottom"===d.toolbarPlacement&&b.append(g),b):("top"===d.toolbarPlacement&&f.append(g),B()&&f.append(a("
        • ").addClass(d.collapse&&A()?"collapse in":"").append(c)),"default"===d.toolbarPlacement&&f.append(g),A()&&f.append(a("
        • ").addClass(d.collapse&&B()?"collapse":"").append(e)),"bottom"===d.toolbarPlacement&&f.append(g),b.append(f))},H=function(){var b,e={};return b=c.is("input")||d.inline?c.data():c.find("input").data(),b.dateOptions&&b.dateOptions instanceof Object&&(e=a.extend(!0,e,b.dateOptions)),a.each(d,function(a){var c="date"+a.charAt(0).toUpperCase()+a.slice(1);void 0!==b[c]&&(e[a]=b[c])}),e},I=function(){var b,e=(n||c).position(),f=(n||c).offset(),g=d.widgetPositioning.vertical,h=d.widgetPositioning.horizontal;if(d.widgetParent)b=d.widgetParent.append(o);else if(c.is("input"))b=c.after(o).parent();else{if(d.inline)return void(b=c.append(o));b=c,c.children().first().after(o)}if("auto"===g&&(g=f.top+1.5*o.height()>=a(window).height()+a(window).scrollTop()&&o.height()+c.outerHeight()a(window).width()?"right":"left"),"top"===g?o.addClass("top").removeClass("bottom"):o.addClass("bottom").removeClass("top"),"right"===h?o.addClass("pull-right"):o.removeClass("pull-right"),"static"===b.css("position")&&(b=b.parents().filter(function(){return"static"!==a(this).css("position")}).first()),0===b.length)throw new Error("datetimepicker component should be placed within a non-static positioned container");o.css({top:"top"===g?"auto":e.top+c.outerHeight(),bottom:"top"===g?b.outerHeight()-(b===c?0:e.top):"auto",left:"left"===h?b===c?0:e.left:"auto",right:"left"===h?"auto":b.outerWidth()-c.outerWidth()-(b===c?0:e.left)})},J=function(a){"dp.change"===a.type&&(a.date&&a.date.isSame(a.oldDate)||!a.date&&!a.oldDate)||c.trigger(a)},K=function(a){"y"===a&&(a="YYYY"),J({type:"dp.update",change:a,viewDate:f.clone()})},L=function(a){o&&(a&&(k=Math.max(p,Math.min(3,k+a))),o.find(".datepicker > div").hide().filter(".datepicker-"+q[k].clsName).show())},M=function(){var b=a("
        "),c=f.clone().startOf("w").startOf("d");for(d.calendarWeeks===!0&&b.append(a(""),d.calendarWeeks&&c.append('"),j.push(c)),k=["day"],b.isBefore(f,"M")&&k.push("old"),b.isAfter(f,"M")&&k.push("new"),b.isSame(e,"d")&&!m&&k.push("active"),R(b,"d")||k.push("disabled"),b.isSame(y(),"d")&&k.push("today"),0!==b.day()&&6!==b.day()||k.push("weekend"),J({type:"dp.classify",date:b,classNames:k}),c.append('"),b.add(1,"d");h.find("tbody").empty().append(j),T(),U(),V()}},X=function(){var b=o.find(".timepicker-hours table"),c=f.clone().startOf("d"),d=[],e=a("");for(f.hour()>11&&!h&&c.hour(12);c.isSame(f,"d")&&(h||f.hour()<12&&c.hour()<12||f.hour()>11);)c.hour()%4===0&&(e=a(""),d.push(e)),e.append('"),c.add(1,"h");b.empty().append(d)},Y=function(){for(var b=o.find(".timepicker-minutes table"),c=f.clone().startOf("h"),e=[],g=a(""),h=1===d.stepping?5:d.stepping;f.isSame(c,"h");)c.minute()%(4*h)===0&&(g=a(""),e.push(g)),g.append('"),c.add(h,"m");b.empty().append(e)},Z=function(){for(var b=o.find(".timepicker-seconds table"),c=f.clone().startOf("m"),d=[],e=a("");f.isSame(c,"m");)c.second()%20===0&&(e=a(""),d.push(e)),e.append('"),c.add(5,"s");b.empty().append(d)},$=function(){var a,b,c=o.find(".timepicker span[data-time-component]");h||(a=o.find(".timepicker [data-action=togglePeriod]"),b=e.clone().add(e.hours()>=12?-12:12,"h"),a.text(e.format("A")),R(b,"h")?a.removeClass("disabled"):a.addClass("disabled")),c.filter("[data-time-component=hours]").text(e.format(h?"HH":"hh")),c.filter("[data-time-component=minutes]").text(e.format("mm")),c.filter("[data-time-component=seconds]").text(e.format("ss")),X(),Y(),Z()},_=function(){o&&(W(),$())},aa=function(a){var b=m?null:e;if(!a)return m=!0,g.val(""),c.data("date",""),J({type:"dp.change",date:!1,oldDate:b}),void _();if(a=a.clone().locale(d.locale),x()&&a.tz(d.timeZone),1!==d.stepping)for(a.minutes(Math.round(a.minutes()/d.stepping)*d.stepping).seconds(0);d.minDate&&a.isBefore(d.minDate);)a.add(d.stepping,"minutes");R(a)?(e=a,f=e.clone(),g.val(e.format(i)),c.data("date",e.format(i)),m=!1,_(),J({type:"dp.change",date:e.clone(),oldDate:b})):(d.keepInvalid?J({type:"dp.change",date:a,oldDate:b}):g.val(m?"":e.format(i)),J({type:"dp.error",date:a,oldDate:b}))},ba=function(){var b=!1;return o?(o.find(".collapse").each(function(){var c=a(this).data("collapse");return!c||!c.transitioning||(b=!0,!1)}),b?l:(n&&n.hasClass("btn")&&n.toggleClass("active"),o.hide(),a(window).off("resize",I),o.off("click","[data-action]"),o.off("mousedown",!1),o.remove(),o=!1,J({type:"dp.hide",date:e.clone()}),g.blur(),f=e.clone(),l)):l},ca=function(){aa(null)},da=function(a){return void 0===d.parseInputDate?(!b.isMoment(a)||a instanceof Date)&&(a=y(a)):a=d.parseInputDate(a),a},ea={next:function(){var a=q[k].navFnc;f.add(q[k].navStep,a),W(),K(a)},previous:function(){var a=q[k].navFnc;f.subtract(q[k].navStep,a),W(),K(a)},pickerSwitch:function(){L(1)},selectMonth:function(b){var c=a(b.target).closest("tbody").find("span").index(a(b.target));f.month(c),k===p?(aa(e.clone().year(f.year()).month(f.month())),d.inline||ba()):(L(-1),W()),K("M")},selectYear:function(b){var c=parseInt(a(b.target).text(),10)||0;f.year(c),k===p?(aa(e.clone().year(f.year())),d.inline||ba()):(L(-1),W()),K("YYYY")},selectDecade:function(b){var c=parseInt(a(b.target).data("selection"),10)||0;f.year(c),k===p?(aa(e.clone().year(f.year())),d.inline||ba()):(L(-1),W()),K("YYYY")},selectDay:function(b){var c=f.clone();a(b.target).is(".old")&&c.subtract(1,"M"),a(b.target).is(".new")&&c.add(1,"M"),aa(c.date(parseInt(a(b.target).text(),10))),A()||d.keepOpen||d.inline||ba()},incrementHours:function(){var a=e.clone().add(1,"h");R(a,"h")&&aa(a)},incrementMinutes:function(){var a=e.clone().add(d.stepping,"m");R(a,"m")&&aa(a)},incrementSeconds:function(){var a=e.clone().add(1,"s");R(a,"s")&&aa(a)},decrementHours:function(){var a=e.clone().subtract(1,"h");R(a,"h")&&aa(a)},decrementMinutes:function(){var a=e.clone().subtract(d.stepping,"m");R(a,"m")&&aa(a)},decrementSeconds:function(){var a=e.clone().subtract(1,"s");R(a,"s")&&aa(a)},togglePeriod:function(){aa(e.clone().add(e.hours()>=12?-12:12,"h"))},togglePicker:function(b){var c,e=a(b.target),f=e.closest("ul"),g=f.find(".in"),h=f.find(".collapse:not(.in)");if(g&&g.length){if(c=g.data("collapse"),c&&c.transitioning)return;g.collapse?(g.collapse("hide"),h.collapse("show")):(g.removeClass("in"),h.addClass("in")),e.is("span")?e.toggleClass(d.icons.time+" "+d.icons.date):e.find("span").toggleClass(d.icons.time+" "+d.icons.date)}},showPicker:function(){o.find(".timepicker > div:not(.timepicker-picker)").hide(),o.find(".timepicker .timepicker-picker").show()},showHours:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-hours").show()},showMinutes:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-minutes").show()},showSeconds:function(){o.find(".timepicker .timepicker-picker").hide(),o.find(".timepicker .timepicker-seconds").show()},selectHour:function(b){var c=parseInt(a(b.target).text(),10);h||(e.hours()>=12?12!==c&&(c+=12):12===c&&(c=0)),aa(e.clone().hours(c)),ea.showPicker.call(l)},selectMinute:function(b){aa(e.clone().minutes(parseInt(a(b.target).text(),10))),ea.showPicker.call(l)},selectSecond:function(b){aa(e.clone().seconds(parseInt(a(b.target).text(),10))),ea.showPicker.call(l)},clear:ca,today:function(){var a=y();R(a,"d")&&aa(a)},close:ba},fa=function(b){return!a(b.currentTarget).is(".disabled")&&(ea[a(b.currentTarget).data("action")].apply(l,arguments),!1)},ga=function(){var b,c={year:function(a){return a.month(0).date(1).hours(0).seconds(0).minutes(0)},month:function(a){return a.date(1).hours(0).seconds(0).minutes(0)},day:function(a){return a.hours(0).seconds(0).minutes(0)},hour:function(a){return a.seconds(0).minutes(0)},minute:function(a){return a.seconds(0)}};return g.prop("disabled")||!d.ignoreReadonly&&g.prop("readonly")||o?l:(void 0!==g.val()&&0!==g.val().trim().length?aa(da(g.val().trim())):m&&d.useCurrent&&(d.inline||g.is("input")&&0===g.val().trim().length)&&(b=y(),"string"==typeof d.useCurrent&&(b=c[d.useCurrent](b)),aa(b)),o=G(),M(),S(),o.find(".timepicker-hours").hide(),o.find(".timepicker-minutes").hide(),o.find(".timepicker-seconds").hide(),_(),L(),a(window).on("resize",I),o.on("click","[data-action]",fa),o.on("mousedown",!1),n&&n.hasClass("btn")&&n.toggleClass("active"),I(),o.show(),d.focusOnShow&&!g.is(":focus")&&g.focus(),J({type:"dp.show"}),l)},ha=function(){return o?ba():ga()},ia=function(a){var b,c,e,f,g=null,h=[],i={},j=a.which,k="p";w[j]=k;for(b in w)w.hasOwnProperty(b)&&w[b]===k&&(h.push(b),parseInt(b,10)!==j&&(i[b]=!0));for(b in d.keyBinds)if(d.keyBinds.hasOwnProperty(b)&&"function"==typeof d.keyBinds[b]&&(e=b.split(" "),e.length===h.length&&v[j]===e[e.length-1])){for(f=!0,c=e.length-2;c>=0;c--)if(!(v[e[c]]in i)){f=!1;break}if(f){g=d.keyBinds[b];break}}g&&(g.call(l,o),a.stopPropagation(),a.preventDefault())},ja=function(a){w[a.which]="r",a.stopPropagation(),a.preventDefault()},ka=function(b){var c=a(b.target).val().trim(),d=c?da(c):null;return aa(d),b.stopImmediatePropagation(),!1},la=function(){g.on({change:ka,blur:d.debug?"":ba,keydown:ia,keyup:ja,focus:d.allowInputToggle?ga:""}),c.is("input")?g.on({focus:ga}):n&&(n.on("click",ha),n.on("mousedown",!1))},ma=function(){g.off({change:ka,blur:blur,keydown:ia,keyup:ja,focus:d.allowInputToggle?ba:""}),c.is("input")?g.off({focus:ga}):n&&(n.off("click",ha),n.off("mousedown",!1))},na=function(b){var c={};return a.each(b,function(){var a=da(this);a.isValid()&&(c[a.format("YYYY-MM-DD")]=!0)}),!!Object.keys(c).length&&c},oa=function(b){var c={};return a.each(b,function(){c[this]=!0}),!!Object.keys(c).length&&c},pa=function(){var a=d.format||"L LT";i=a.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,function(a){var b=e.localeData().longDateFormat(a)||a;return b.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,function(a){return e.localeData().longDateFormat(a)||a})}),j=d.extraFormats?d.extraFormats.slice():[],j.indexOf(a)<0&&j.indexOf(i)<0&&j.push(i),h=i.toLowerCase().indexOf("a")<1&&i.replace(/\[.*?\]/g,"").indexOf("h")<1,z("y")&&(p=2),z("M")&&(p=1),z("d")&&(p=0),k=Math.max(p,k),m||aa(e)};if(l.destroy=function(){ba(),ma(),c.removeData("DateTimePicker"),c.removeData("date")},l.toggle=ha,l.show=ga,l.hide=ba,l.disable=function(){return ba(),n&&n.hasClass("btn")&&n.addClass("disabled"),g.prop("disabled",!0),l},l.enable=function(){return n&&n.hasClass("btn")&&n.removeClass("disabled"),g.prop("disabled",!1),l},l.ignoreReadonly=function(a){if(0===arguments.length)return d.ignoreReadonly;if("boolean"!=typeof a)throw new TypeError("ignoreReadonly () expects a boolean parameter");return d.ignoreReadonly=a,l},l.options=function(b){if(0===arguments.length)return a.extend(!0,{},d);if(!(b instanceof Object))throw new TypeError("options() options parameter should be an object");return a.extend(!0,d,b),a.each(d,function(a,b){if(void 0===l[a])throw new TypeError("option "+a+" is not recognized!");l[a](b)}),l},l.date=function(a){if(0===arguments.length)return m?null:e.clone();if(!(null===a||"string"==typeof a||b.isMoment(a)||a instanceof Date))throw new TypeError("date() parameter must be one of [null, string, moment or Date]");return aa(null===a?null:da(a)),l},l.format=function(a){if(0===arguments.length)return d.format;if("string"!=typeof a&&("boolean"!=typeof a||a!==!1))throw new TypeError("format() expects a string or boolean:false parameter "+a);return d.format=a,i&&pa(),l},l.timeZone=function(a){if(0===arguments.length)return d.timeZone;if("string"!=typeof a)throw new TypeError("newZone() expects a string parameter");return d.timeZone=a,l},l.dayViewHeaderFormat=function(a){if(0===arguments.length)return d.dayViewHeaderFormat;if("string"!=typeof a)throw new TypeError("dayViewHeaderFormat() expects a string parameter");return d.dayViewHeaderFormat=a,l},l.extraFormats=function(a){if(0===arguments.length)return d.extraFormats;if(a!==!1&&!(a instanceof Array))throw new TypeError("extraFormats() expects an array or false parameter");return d.extraFormats=a,j&&pa(),l},l.disabledDates=function(b){if(0===arguments.length)return d.disabledDates?a.extend({},d.disabledDates):d.disabledDates;if(!b)return d.disabledDates=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledDates() expects an array parameter");return d.disabledDates=na(b),d.enabledDates=!1,_(),l},l.enabledDates=function(b){if(0===arguments.length)return d.enabledDates?a.extend({},d.enabledDates):d.enabledDates;if(!b)return d.enabledDates=!1,_(),l;if(!(b instanceof Array))throw new TypeError("enabledDates() expects an array parameter");return d.enabledDates=na(b),d.disabledDates=!1,_(),l},l.daysOfWeekDisabled=function(a){if(0===arguments.length)return d.daysOfWeekDisabled.splice(0);if("boolean"==typeof a&&!a)return d.daysOfWeekDisabled=!1,_(),l;if(!(a instanceof Array))throw new TypeError("daysOfWeekDisabled() expects an array parameter");if(d.daysOfWeekDisabled=a.reduce(function(a,b){return b=parseInt(b,10),b>6||b<0||isNaN(b)?a:(a.indexOf(b)===-1&&a.push(b),a)},[]).sort(),d.useCurrent&&!d.keepInvalid){for(var b=0;!R(e,"d");){if(e.add(1,"d"),31===b)throw"Tried 31 times to find a valid date";b++}aa(e)}return _(),l},l.maxDate=function(a){if(0===arguments.length)return d.maxDate?d.maxDate.clone():d.maxDate;if("boolean"==typeof a&&a===!1)return d.maxDate=!1,_(),l;"string"==typeof a&&("now"!==a&&"moment"!==a||(a=y()));var b=da(a);if(!b.isValid())throw new TypeError("maxDate() Could not parse date parameter: "+a);if(d.minDate&&b.isBefore(d.minDate))throw new TypeError("maxDate() date parameter is before options.minDate: "+b.format(i));return d.maxDate=b,d.useCurrent&&!d.keepInvalid&&e.isAfter(a)&&aa(d.maxDate),f.isAfter(b)&&(f=b.clone().subtract(d.stepping,"m")),_(),l},l.minDate=function(a){if(0===arguments.length)return d.minDate?d.minDate.clone():d.minDate;if("boolean"==typeof a&&a===!1)return d.minDate=!1,_(),l;"string"==typeof a&&("now"!==a&&"moment"!==a||(a=y()));var b=da(a);if(!b.isValid())throw new TypeError("minDate() Could not parse date parameter: "+a);if(d.maxDate&&b.isAfter(d.maxDate))throw new TypeError("minDate() date parameter is after options.maxDate: "+b.format(i));return d.minDate=b,d.useCurrent&&!d.keepInvalid&&e.isBefore(a)&&aa(d.minDate),f.isBefore(b)&&(f=b.clone().add(d.stepping,"m")),_(),l},l.defaultDate=function(a){if(0===arguments.length)return d.defaultDate?d.defaultDate.clone():d.defaultDate;if(!a)return d.defaultDate=!1,l;"string"==typeof a&&(a="now"===a||"moment"===a?y():y(a));var b=da(a);if(!b.isValid())throw new TypeError("defaultDate() Could not parse date parameter: "+a);if(!R(b))throw new TypeError("defaultDate() date passed is invalid according to component setup validations");return d.defaultDate=b,(d.defaultDate&&d.inline||""===g.val().trim())&&aa(d.defaultDate),l},l.locale=function(a){if(0===arguments.length)return d.locale;if(!b.localeData(a))throw new TypeError("locale() locale "+a+" is not loaded from moment locales!");return d.locale=a,e.locale(d.locale),f.locale(d.locale),i&&pa(),o&&(ba(),ga()),l},l.stepping=function(a){return 0===arguments.length?d.stepping:(a=parseInt(a,10),(isNaN(a)||a<1)&&(a=1),d.stepping=a,l)},l.useCurrent=function(a){var b=["year","month","day","hour","minute"];if(0===arguments.length)return d.useCurrent;if("boolean"!=typeof a&&"string"!=typeof a)throw new TypeError("useCurrent() expects a boolean or string parameter");if("string"==typeof a&&b.indexOf(a.toLowerCase())===-1)throw new TypeError("useCurrent() expects a string parameter of "+b.join(", "));return d.useCurrent=a,l},l.collapse=function(a){if(0===arguments.length)return d.collapse;if("boolean"!=typeof a)throw new TypeError("collapse() expects a boolean parameter");return d.collapse===a?l:(d.collapse=a,o&&(ba(),ga()),l)},l.icons=function(b){if(0===arguments.length)return a.extend({},d.icons);if(!(b instanceof Object))throw new TypeError("icons() expects parameter to be an Object");return a.extend(d.icons,b),o&&(ba(),ga()),l},l.tooltips=function(b){if(0===arguments.length)return a.extend({},d.tooltips);if(!(b instanceof Object))throw new TypeError("tooltips() expects parameter to be an Object");return a.extend(d.tooltips,b),o&&(ba(),ga()),l},l.useStrict=function(a){if(0===arguments.length)return d.useStrict;if("boolean"!=typeof a)throw new TypeError("useStrict() expects a boolean parameter");return d.useStrict=a,l},l.sideBySide=function(a){if(0===arguments.length)return d.sideBySide;if("boolean"!=typeof a)throw new TypeError("sideBySide() expects a boolean parameter");return d.sideBySide=a,o&&(ba(),ga()),l},l.viewMode=function(a){if(0===arguments.length)return d.viewMode;if("string"!=typeof a)throw new TypeError("viewMode() expects a string parameter");if(r.indexOf(a)===-1)throw new TypeError("viewMode() parameter must be one of ("+r.join(", ")+") value");return d.viewMode=a,k=Math.max(r.indexOf(a),p),L(),l},l.toolbarPlacement=function(a){if(0===arguments.length)return d.toolbarPlacement;if("string"!=typeof a)throw new TypeError("toolbarPlacement() expects a string parameter");if(u.indexOf(a)===-1)throw new TypeError("toolbarPlacement() parameter must be one of ("+u.join(", ")+") value");return d.toolbarPlacement=a,o&&(ba(),ga()),l},l.widgetPositioning=function(b){if(0===arguments.length)return a.extend({},d.widgetPositioning);if("[object Object]"!=={}.toString.call(b))throw new TypeError("widgetPositioning() expects an object variable");if(b.horizontal){if("string"!=typeof b.horizontal)throw new TypeError("widgetPositioning() horizontal variable must be a string");if(b.horizontal=b.horizontal.toLowerCase(),t.indexOf(b.horizontal)===-1)throw new TypeError("widgetPositioning() expects horizontal parameter to be one of ("+t.join(", ")+")");d.widgetPositioning.horizontal=b.horizontal}if(b.vertical){if("string"!=typeof b.vertical)throw new TypeError("widgetPositioning() vertical variable must be a string");if(b.vertical=b.vertical.toLowerCase(),s.indexOf(b.vertical)===-1)throw new TypeError("widgetPositioning() expects vertical parameter to be one of ("+s.join(", ")+")");d.widgetPositioning.vertical=b.vertical}return _(),l},l.calendarWeeks=function(a){if(0===arguments.length)return d.calendarWeeks;if("boolean"!=typeof a)throw new TypeError("calendarWeeks() expects parameter to be a boolean value");return d.calendarWeeks=a,_(),l},l.showTodayButton=function(a){if(0===arguments.length)return d.showTodayButton;if("boolean"!=typeof a)throw new TypeError("showTodayButton() expects a boolean parameter");return d.showTodayButton=a,o&&(ba(),ga()),l},l.showClear=function(a){if(0===arguments.length)return d.showClear;if("boolean"!=typeof a)throw new TypeError("showClear() expects a boolean parameter");return d.showClear=a,o&&(ba(),ga()),l},l.widgetParent=function(b){if(0===arguments.length)return d.widgetParent;if("string"==typeof b&&(b=a(b)),null!==b&&"string"!=typeof b&&!(b instanceof a))throw new TypeError("widgetParent() expects a string or a jQuery object parameter");return d.widgetParent=b,o&&(ba(),ga()),l},l.keepOpen=function(a){if(0===arguments.length)return d.keepOpen;if("boolean"!=typeof a)throw new TypeError("keepOpen() expects a boolean parameter");return d.keepOpen=a,l},l.focusOnShow=function(a){if(0===arguments.length)return d.focusOnShow;if("boolean"!=typeof a)throw new TypeError("focusOnShow() expects a boolean parameter");return d.focusOnShow=a,l},l.inline=function(a){if(0===arguments.length)return d.inline;if("boolean"!=typeof a)throw new TypeError("inline() expects a boolean parameter");return d.inline=a,l},l.clear=function(){return ca(),l},l.keyBinds=function(a){return 0===arguments.length?d.keyBinds:(d.keyBinds=a,l)},l.getMoment=function(a){return y(a)},l.debug=function(a){if("boolean"!=typeof a)throw new TypeError("debug() expects a boolean parameter");return d.debug=a,l},l.allowInputToggle=function(a){if(0===arguments.length)return d.allowInputToggle;if("boolean"!=typeof a)throw new TypeError("allowInputToggle() expects a boolean parameter");return d.allowInputToggle=a,l},l.showClose=function(a){if(0===arguments.length)return d.showClose;if("boolean"!=typeof a)throw new TypeError("showClose() expects a boolean parameter");return d.showClose=a,l},l.keepInvalid=function(a){if(0===arguments.length)return d.keepInvalid;if("boolean"!=typeof a)throw new TypeError("keepInvalid() expects a boolean parameter"); +return d.keepInvalid=a,l},l.datepickerInput=function(a){if(0===arguments.length)return d.datepickerInput;if("string"!=typeof a)throw new TypeError("datepickerInput() expects a string parameter");return d.datepickerInput=a,l},l.parseInputDate=function(a){if(0===arguments.length)return d.parseInputDate;if("function"!=typeof a)throw new TypeError("parseInputDate() sholud be as function");return d.parseInputDate=a,l},l.disabledTimeIntervals=function(b){if(0===arguments.length)return d.disabledTimeIntervals?a.extend({},d.disabledTimeIntervals):d.disabledTimeIntervals;if(!b)return d.disabledTimeIntervals=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledTimeIntervals() expects an array parameter");return d.disabledTimeIntervals=b,_(),l},l.disabledHours=function(b){if(0===arguments.length)return d.disabledHours?a.extend({},d.disabledHours):d.disabledHours;if(!b)return d.disabledHours=!1,_(),l;if(!(b instanceof Array))throw new TypeError("disabledHours() expects an array parameter");if(d.disabledHours=oa(b),d.enabledHours=!1,d.useCurrent&&!d.keepInvalid){for(var c=0;!R(e,"h");){if(e.add(1,"h"),24===c)throw"Tried 24 times to find a valid date";c++}aa(e)}return _(),l},l.enabledHours=function(b){if(0===arguments.length)return d.enabledHours?a.extend({},d.enabledHours):d.enabledHours;if(!b)return d.enabledHours=!1,_(),l;if(!(b instanceof Array))throw new TypeError("enabledHours() expects an array parameter");if(d.enabledHours=oa(b),d.disabledHours=!1,d.useCurrent&&!d.keepInvalid){for(var c=0;!R(e,"h");){if(e.add(1,"h"),24===c)throw"Tried 24 times to find a valid date";c++}aa(e)}return _(),l},l.viewDate=function(a){if(0===arguments.length)return f.clone();if(!a)return f=e.clone(),l;if(!("string"==typeof a||b.isMoment(a)||a instanceof Date))throw new TypeError("viewDate() parameter must be one of [string, moment or Date]");return f=da(a),K(),l},c.is("input"))g=c;else if(g=c.find(d.datepickerInput),0===g.length)g=c.find("input");else if(!g.is("input"))throw new Error('CSS class "'+d.datepickerInput+'" cannot be applied to non input element');if(c.hasClass("input-group")&&(n=0===c.find(".datepickerbutton").length?c.find(".input-group-addon"):c.find(".datepickerbutton")),!d.inline&&!g.is("input"))throw new Error("Could not initialize DateTimePicker without an input element");return e=y(),f=e.clone(),a.extend(!0,d,H()),l.options(d),pa(),la(),g.prop("disabled")&&l.disable(),g.is("input")&&0!==g.val().trim().length?aa(da(g.val().trim())):d.defaultDate&&void 0===g.attr("placeholder")&&aa(d.defaultDate),d.inline&&ga(),l};return a.fn.datetimepicker=function(b){b=b||{};var d,e=Array.prototype.slice.call(arguments,1),f=!0,g=["destroy","hide","show","toggle"];if("object"==typeof b)return this.each(function(){var d,e=a(this);e.data("DateTimePicker")||(d=a.extend(!0,{},a.fn.datetimepicker.defaults,b),e.data("DateTimePicker",c(e,d)))});if("string"==typeof b)return this.each(function(){var c=a(this),g=c.data("DateTimePicker");if(!g)throw new Error('bootstrap-datetimepicker("'+b+'") method was called on an element that is not using DateTimePicker');d=g[b].apply(g,e),f=d===g}),f||a.inArray(b,g)>-1?this:d;throw new TypeError("Invalid arguments for DateTimePicker: "+b)},a.fn.datetimepicker.defaults={timeZone:"",format:!1,dayViewHeaderFormat:"MMMM YYYY",extraFormats:!1,stepping:1,minDate:!1,maxDate:!1,useCurrent:!0,collapse:!0,locale:b.locale(),defaultDate:!1,disabledDates:!1,enabledDates:!1,icons:{time:"glyphicon glyphicon-time",date:"glyphicon glyphicon-calendar",up:"glyphicon glyphicon-chevron-up",down:"glyphicon glyphicon-chevron-down",previous:"glyphicon glyphicon-chevron-left",next:"glyphicon glyphicon-chevron-right",today:"glyphicon glyphicon-screenshot",clear:"glyphicon glyphicon-trash",close:"glyphicon glyphicon-remove"},tooltips:{today:"Go to today",clear:"Clear selection",close:"Close the picker",selectMonth:"Select Month",prevMonth:"Previous Month",nextMonth:"Next Month",selectYear:"Select Year",prevYear:"Previous Year",nextYear:"Next Year",selectDecade:"Select Decade",prevDecade:"Previous Decade",nextDecade:"Next Decade",prevCentury:"Previous Century",nextCentury:"Next Century",pickHour:"Pick Hour",incrementHour:"Increment Hour",decrementHour:"Decrement Hour",pickMinute:"Pick Minute",incrementMinute:"Increment Minute",decrementMinute:"Decrement Minute",pickSecond:"Pick Second",incrementSecond:"Increment Second",decrementSecond:"Decrement Second",togglePeriod:"Toggle Period",selectTime:"Select Time"},useStrict:!1,sideBySide:!1,daysOfWeekDisabled:!1,calendarWeeks:!1,viewMode:"days",toolbarPlacement:"default",showTodayButton:!1,showClear:!1,showClose:!1,widgetPositioning:{horizontal:"auto",vertical:"auto"},widgetParent:null,ignoreReadonly:!1,keepOpen:!1,focusOnShow:!0,inline:!1,keepInvalid:!1,datepickerInput:".datepickerinput",keyBinds:{up:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().subtract(7,"d")):this.date(b.clone().add(this.stepping(),"m"))}},down:function(a){if(!a)return void this.show();var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().add(7,"d")):this.date(b.clone().subtract(this.stepping(),"m"))},"control up":function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().subtract(1,"y")):this.date(b.clone().add(1,"h"))}},"control down":function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")?this.date(b.clone().add(1,"y")):this.date(b.clone().subtract(1,"h"))}},left:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().subtract(1,"d"))}},right:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().add(1,"d"))}},pageUp:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().subtract(1,"M"))}},pageDown:function(a){if(a){var b=this.date()||this.getMoment();a.find(".datepicker").is(":visible")&&this.date(b.clone().add(1,"M"))}},enter:function(){this.hide()},escape:function(){this.hide()},"control space":function(a){a&&a.find(".timepicker").is(":visible")&&a.find('.btn[data-action="togglePeriod"]').click()},t:function(){this.date(this.getMoment())},delete:function(){this.clear()}},debug:!1,allowInputToggle:!1,disabledTimeIntervals:!1,disabledHours:!1,enabledHours:!1,viewDate:!1},a.fn.datetimepicker}); \ No newline at end of file diff --git a/public/static/js/bootstrap-table-1.21.4.min.js b/public/static/js/bootstrap-table-1.21.4.min.js new file mode 100644 index 0000000..cf43808 --- /dev/null +++ b/public/static/js/bootstrap-table-1.21.4.min.js @@ -0,0 +1,10 @@ +/** + * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) + * + * @version v1.21.4 + * @homepage https://bootstrap-table.com + * @author wenzhixin (http://wenzhixin.net.cn/) + * @license MIT + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).BootstrapTable=e(t.jQuery)}(this,(function(t){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},e(t)}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){for(var i=0;it.length)&&(e=t.length);for(var i=0,n=new Array(e);i=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var r,a=!0,l=!1;return{s:function(){i=i.call(t)},n:function(){var t=i.next();return a=t.done,t},e:function(t){l=!0,r=t},f:function(){try{a||null==i.return||i.return()}finally{if(l)throw r}}}}var h="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},u=function(t){return t&&t.Math==Math&&t},d=u("object"==typeof globalThis&&globalThis)||u("object"==typeof window&&window)||u("object"==typeof self&&self)||u("object"==typeof h&&h)||function(){return this}()||Function("return this")(),p={},f=function(t){try{return!!t()}catch(t){return!0}},g=!f((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),v=!f((function(){var t=function(){}.bind();return"function"!=typeof t||t.hasOwnProperty("prototype")})),b=v,m=Function.prototype.call,y=b?m.bind(m):function(){return m.apply(m,arguments)},w={},S={}.propertyIsEnumerable,x=Object.getOwnPropertyDescriptor,k=x&&!S.call({1:2},1);w.f=k?function(t){var e=x(this,t);return!!e&&e.enumerable}:S;var O,C,T=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},I=v,P=Function.prototype,A=P.call,$=I&&P.bind.bind(A,A),R=I?$:function(t){return function(){return A.apply(t,arguments)}},E=R,j=E({}.toString),_=E("".slice),F=function(t){return _(j(t),8,-1)},D=f,N=F,V=Object,B=R("".split),L=D((function(){return!V("z").propertyIsEnumerable(0)}))?function(t){return"String"==N(t)?B(t,""):V(t)}:V,H=function(t){return null==t},M=H,U=TypeError,z=function(t){if(M(t))throw U("Can't call method on "+t);return t},q=L,W=z,G=function(t){return q(W(t))},K="object"==typeof document&&document.all,Y={all:K,IS_HTMLDDA:void 0===K&&void 0!==K},J=Y.all,X=Y.IS_HTMLDDA?function(t){return"function"==typeof t||t===J}:function(t){return"function"==typeof t},Q=X,Z=Y.all,tt=Y.IS_HTMLDDA?function(t){return"object"==typeof t?null!==t:Q(t)||t===Z}:function(t){return"object"==typeof t?null!==t:Q(t)},et=d,it=X,nt=function(t){return it(t)?t:void 0},ot=function(t,e){return arguments.length<2?nt(et[t]):et[t]&&et[t][e]},rt=R({}.isPrototypeOf),at="undefined"!=typeof navigator&&String(navigator.userAgent)||"",st=d,lt=at,ct=st.process,ht=st.Deno,ut=ct&&ct.versions||ht&&ht.version,dt=ut&&ut.v8;dt&&(C=(O=dt.split("."))[0]>0&&O[0]<4?1:+(O[0]+O[1])),!C&<&&(!(O=lt.match(/Edge\/(\d+)/))||O[1]>=74)&&(O=lt.match(/Chrome\/(\d+)/))&&(C=+O[1]);var pt=C,ft=pt,gt=f,vt=!!Object.getOwnPropertySymbols&&!gt((function(){var t=Symbol();return!String(t)||!(Object(t)instanceof Symbol)||!Symbol.sham&&ft&&ft<41})),bt=vt&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,mt=ot,yt=X,wt=rt,St=Object,xt=bt?function(t){return"symbol"==typeof t}:function(t){var e=mt("Symbol");return yt(e)&&wt(e.prototype,St(t))},kt=String,Ot=function(t){try{return kt(t)}catch(t){return"Object"}},Ct=X,Tt=Ot,It=TypeError,Pt=function(t){if(Ct(t))return t;throw It(Tt(t)+" is not a function")},At=Pt,$t=H,Rt=function(t,e){var i=t[e];return $t(i)?void 0:At(i)},Et=y,jt=X,_t=tt,Ft=TypeError,Dt={},Nt={get exports(){return Dt},set exports(t){Dt=t}},Vt=d,Bt=Object.defineProperty,Lt=function(t,e){try{Bt(Vt,t,{value:e,configurable:!0,writable:!0})}catch(i){Vt[t]=e}return e},Ht=Lt,Mt="__core-js_shared__",Ut=d[Mt]||Ht(Mt,{}),zt=Ut;(Nt.exports=function(t,e){return zt[t]||(zt[t]=void 0!==e?e:{})})("versions",[]).push({version:"3.29.0",mode:"global",copyright:"© 2014-2023 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.29.0/LICENSE",source:"https://github.com/zloirock/core-js"});var qt=z,Wt=Object,Gt=function(t){return Wt(qt(t))},Kt=Gt,Yt=R({}.hasOwnProperty),Jt=Object.hasOwn||function(t,e){return Yt(Kt(t),e)},Xt=R,Qt=0,Zt=Math.random(),te=Xt(1..toString),ee=function(t){return"Symbol("+(void 0===t?"":t)+")_"+te(++Qt+Zt,36)},ie=Dt,ne=Jt,oe=ee,re=vt,ae=bt,se=d.Symbol,le=ie("wks"),ce=ae?se.for||se:se&&se.withoutSetter||oe,he=function(t){return ne(le,t)||(le[t]=re&&ne(se,t)?se[t]:ce("Symbol."+t)),le[t]},ue=y,de=tt,pe=xt,fe=Rt,ge=function(t,e){var i,n;if("string"===e&&jt(i=t.toString)&&!_t(n=Et(i,t)))return n;if(jt(i=t.valueOf)&&!_t(n=Et(i,t)))return n;if("string"!==e&&jt(i=t.toString)&&!_t(n=Et(i,t)))return n;throw Ft("Can't convert object to primitive value")},ve=TypeError,be=he("toPrimitive"),me=function(t,e){if(!de(t)||pe(t))return t;var i,n=fe(t,be);if(n){if(void 0===e&&(e="default"),i=ue(n,t,e),!de(i)||pe(i))return i;throw ve("Can't convert object to primitive value")}return void 0===e&&(e="number"),ge(t,e)},ye=me,we=xt,Se=function(t){var e=ye(t,"string");return we(e)?e:e+""},xe=tt,ke=d.document,Oe=xe(ke)&&xe(ke.createElement),Ce=function(t){return Oe?ke.createElement(t):{}},Te=Ce,Ie=!g&&!f((function(){return 7!=Object.defineProperty(Te("div"),"a",{get:function(){return 7}}).a})),Pe=g,Ae=y,$e=w,Re=T,Ee=G,je=Se,_e=Jt,Fe=Ie,De=Object.getOwnPropertyDescriptor;p.f=Pe?De:function(t,e){if(t=Ee(t),e=je(e),Fe)try{return De(t,e)}catch(t){}if(_e(t,e))return Re(!Ae($e.f,t,e),t[e])};var Ne={},Ve=g&&f((function(){return 42!=Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype})),Be=tt,Le=String,He=TypeError,Me=function(t){if(Be(t))return t;throw He(Le(t)+" is not an object")},Ue=g,ze=Ie,qe=Ve,We=Me,Ge=Se,Ke=TypeError,Ye=Object.defineProperty,Je=Object.getOwnPropertyDescriptor,Xe="enumerable",Qe="configurable",Ze="writable";Ne.f=Ue?qe?function(t,e,i){if(We(t),e=Ge(e),We(i),"function"==typeof t&&"prototype"===e&&"value"in i&&Ze in i&&!i.writable){var n=Je(t,e);n&&n.writable&&(t[e]=i.value,i={configurable:Qe in i?i.configurable:n.configurable,enumerable:Xe in i?i.enumerable:n.enumerable,writable:!1})}return Ye(t,e,i)}:Ye:function(t,e,i){if(We(t),e=Ge(e),We(i),ze)try{return Ye(t,e,i)}catch(t){}if("get"in i||"set"in i)throw Ke("Accessors not supported");return"value"in i&&(t[e]=i.value),t};var ti=Ne,ei=T,ii=g?function(t,e,i){return ti.f(t,e,ei(1,i))}:function(t,e,i){return t[e]=i,t},ni={},oi={get exports(){return ni},set exports(t){ni=t}},ri=g,ai=Jt,si=Function.prototype,li=ri&&Object.getOwnPropertyDescriptor,ci=ai(si,"name"),hi={EXISTS:ci,PROPER:ci&&"something"===function(){}.name,CONFIGURABLE:ci&&(!ri||ri&&li(si,"name").configurable)},ui=X,di=Ut,pi=R(Function.toString);ui(di.inspectSource)||(di.inspectSource=function(t){return pi(t)});var fi,gi,vi,bi=di.inspectSource,mi=X,yi=d.WeakMap,wi=mi(yi)&&/native code/.test(String(yi)),Si=ee,xi=Dt("keys"),ki=function(t){return xi[t]||(xi[t]=Si(t))},Oi={},Ci=wi,Ti=d,Ii=tt,Pi=ii,Ai=Jt,$i=Ut,Ri=ki,Ei=Oi,ji="Object already initialized",_i=Ti.TypeError,Fi=Ti.WeakMap;if(Ci||$i.state){var Di=$i.state||($i.state=new Fi);Di.get=Di.get,Di.has=Di.has,Di.set=Di.set,fi=function(t,e){if(Di.has(t))throw _i(ji);return e.facade=t,Di.set(t,e),e},gi=function(t){return Di.get(t)||{}},vi=function(t){return Di.has(t)}}else{var Ni=Ri("state");Ei[Ni]=!0,fi=function(t,e){if(Ai(t,Ni))throw _i(ji);return e.facade=t,Pi(t,Ni,e),e},gi=function(t){return Ai(t,Ni)?t[Ni]:{}},vi=function(t){return Ai(t,Ni)}}var Vi={set:fi,get:gi,has:vi,enforce:function(t){return vi(t)?gi(t):fi(t,{})},getterFor:function(t){return function(e){var i;if(!Ii(e)||(i=gi(e)).type!==t)throw _i("Incompatible receiver, "+t+" required");return i}}},Bi=R,Li=f,Hi=X,Mi=Jt,Ui=g,zi=hi.CONFIGURABLE,qi=bi,Wi=Vi.enforce,Gi=Vi.get,Ki=String,Yi=Object.defineProperty,Ji=Bi("".slice),Xi=Bi("".replace),Qi=Bi([].join),Zi=Ui&&!Li((function(){return 8!==Yi((function(){}),"length",{value:8}).length})),tn=String(String).split("String"),en=oi.exports=function(t,e,i){"Symbol("===Ji(Ki(e),0,7)&&(e="["+Xi(Ki(e),/^Symbol\(([^)]*)\)/,"$1")+"]"),i&&i.getter&&(e="get "+e),i&&i.setter&&(e="set "+e),(!Mi(t,"name")||zi&&t.name!==e)&&(Ui?Yi(t,"name",{value:e,configurable:!0}):t.name=e),Zi&&i&&Mi(i,"arity")&&t.length!==i.arity&&Yi(t,"length",{value:i.arity});try{i&&Mi(i,"constructor")&&i.constructor?Ui&&Yi(t,"prototype",{writable:!1}):t.prototype&&(t.prototype=void 0)}catch(t){}var n=Wi(t);return Mi(n,"source")||(n.source=Qi(tn,"string"==typeof e?e:"")),t};Function.prototype.toString=en((function(){return Hi(this)&&Gi(this).source||qi(this)}),"toString");var nn=X,on=Ne,rn=ni,an=Lt,sn=function(t,e,i,n){n||(n={});var o=n.enumerable,r=void 0!==n.name?n.name:e;if(nn(i)&&rn(i,r,n),n.global)o?t[e]=i:an(e,i);else{try{n.unsafe?t[e]&&(o=!0):delete t[e]}catch(t){}o?t[e]=i:on.f(t,e,{value:i,enumerable:!1,configurable:!n.nonConfigurable,writable:!n.nonWritable})}return t},ln={},cn=Math.ceil,hn=Math.floor,un=Math.trunc||function(t){var e=+t;return(e>0?hn:cn)(e)},dn=function(t){var e=+t;return e!=e||0===e?0:un(e)},pn=dn,fn=Math.max,gn=Math.min,vn=function(t,e){var i=pn(t);return i<0?fn(i+e,0):gn(i,e)},bn=dn,mn=Math.min,yn=function(t){return t>0?mn(bn(t),9007199254740991):0},wn=yn,Sn=function(t){return wn(t.length)},xn=G,kn=vn,On=Sn,Cn=function(t){return function(e,i,n){var o,r=xn(e),a=On(r),s=kn(n,a);if(t&&i!=i){for(;a>s;)if((o=r[s++])!=o)return!0}else for(;a>s;s++)if((t||s in r)&&r[s]===i)return t||s||0;return!t&&-1}},Tn={includes:Cn(!0),indexOf:Cn(!1)},In=Jt,Pn=G,An=Tn.indexOf,$n=Oi,Rn=R([].push),En=function(t,e){var i,n=Pn(t),o=0,r=[];for(i in n)!In($n,i)&&In(n,i)&&Rn(r,i);for(;e.length>o;)In(n,i=e[o++])&&(~An(r,i)||Rn(r,i));return r},jn=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],_n=En,Fn=jn.concat("length","prototype");ln.f=Object.getOwnPropertyNames||function(t){return _n(t,Fn)};var Dn={};Dn.f=Object.getOwnPropertySymbols;var Nn=ot,Vn=ln,Bn=Dn,Ln=Me,Hn=R([].concat),Mn=Nn("Reflect","ownKeys")||function(t){var e=Vn.f(Ln(t)),i=Bn.f;return i?Hn(e,i(t)):e},Un=Jt,zn=Mn,qn=p,Wn=Ne,Gn=f,Kn=X,Yn=/#|\.prototype\./,Jn=function(t,e){var i=Qn[Xn(t)];return i==to||i!=Zn&&(Kn(e)?Gn(e):!!e)},Xn=Jn.normalize=function(t){return String(t).replace(Yn,".").toLowerCase()},Qn=Jn.data={},Zn=Jn.NATIVE="N",to=Jn.POLYFILL="P",eo=Jn,io=d,no=p.f,oo=ii,ro=sn,ao=Lt,so=function(t,e,i){for(var n=zn(e),o=Wn.f,r=qn.f,a=0;ao;)for(var s,l=xo(arguments[o++]),c=r?Co(mo(l),r(l)):mo(l),h=c.length,u=0;h>u;)s=c[u++],fo&&!vo(a,l,s)||(i[s]=l[s]);return i}:ko,Io=To;co({target:"Object",stat:!0,arity:2,forced:Object.assign!==Io},{assign:Io});var Po={};Po[he("toStringTag")]="z";var Ao="[object z]"===String(Po),$o=Ao,Ro=X,Eo=F,jo=he("toStringTag"),_o=Object,Fo="Arguments"==Eo(function(){return arguments}()),Do=$o?Eo:function(t){var e,i,n;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(i=function(t,e){try{return t[e]}catch(t){}}(e=_o(t),jo))?i:Fo?Eo(e):"Object"==(n=Eo(e))&&Ro(e.callee)?"Arguments":n},No=Do,Vo=String,Bo=function(t){if("Symbol"===No(t))throw TypeError("Cannot convert a Symbol value to a string");return Vo(t)},Lo="\t\n\v\f\r                 \u2028\u2029\ufeff",Ho=z,Mo=Bo,Uo=Lo,zo=R("".replace),qo=RegExp("^["+Uo+"]+"),Wo=RegExp("(^|[^"+Uo+"])["+Uo+"]+$"),Go=function(t){return function(e){var i=Mo(Ho(e));return 1&t&&(i=zo(i,qo,"")),2&t&&(i=zo(i,Wo,"$1")),i}},Ko={start:Go(1),end:Go(2),trim:Go(3)},Yo=hi.PROPER,Jo=f,Xo=Lo,Qo=Ko.trim;co({target:"String",proto:!0,forced:function(t){return Jo((function(){return!!Xo[t]()||"​…᠎"!=="​…᠎"[t]()||Yo&&Xo[t].name!==t}))}("trim")},{trim:function(){return Qo(this)}});var Zo=f,tr=function(t,e){var i=[][t];return!!i&&Zo((function(){i.call(null,e||function(){return 1},1)}))},er=co,ir=L,nr=G,or=tr,rr=R([].join);er({target:"Array",proto:!0,forced:ir!=Object||!or("join",",")},{join:function(t){return rr(nr(this),void 0===t?",":t)}});var ar=Me,sr=function(){var t=ar(this),e="";return t.hasIndices&&(e+="d"),t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.dotAll&&(e+="s"),t.unicode&&(e+="u"),t.unicodeSets&&(e+="v"),t.sticky&&(e+="y"),e},lr=f,cr=d.RegExp,hr=lr((function(){var t=cr("a","y");return t.lastIndex=2,null!=t.exec("abcd")})),ur=hr||lr((function(){return!cr("a","y").sticky})),dr={BROKEN_CARET:hr||lr((function(){var t=cr("^r","gy");return t.lastIndex=2,null!=t.exec("str")})),MISSED_STICKY:ur,UNSUPPORTED_Y:hr},pr={},fr=g,gr=Ve,vr=Ne,br=Me,mr=G,yr=po;pr.f=fr&&!gr?Object.defineProperties:function(t,e){br(t);for(var i,n=mr(e),o=yr(e),r=o.length,a=0;r>a;)vr.f(t,i=o[a++],n[i]);return t};var wr,Sr=ot("document","documentElement"),xr=Me,kr=pr,Or=jn,Cr=Oi,Tr=Sr,Ir=Ce,Pr=ki("IE_PROTO"),Ar=function(){},$r=function(t){return"
        ").addClass("cw").text("#"));c.isBefore(f.clone().endOf("w"));)b.append(a("").addClass("dow").text(c.format("dd"))),c.add(1,"d");o.find(".datepicker-days thead").append(b)},N=function(a){return d.disabledDates[a.format("YYYY-MM-DD")]===!0},O=function(a){return d.enabledDates[a.format("YYYY-MM-DD")]===!0},P=function(a){return d.disabledHours[a.format("H")]===!0},Q=function(a){return d.enabledHours[a.format("H")]===!0},R=function(b,c){if(!b.isValid())return!1;if(d.disabledDates&&"d"===c&&N(b))return!1;if(d.enabledDates&&"d"===c&&!O(b))return!1;if(d.minDate&&b.isBefore(d.minDate,c))return!1;if(d.maxDate&&b.isAfter(d.maxDate,c))return!1;if(d.daysOfWeekDisabled&&"d"===c&&d.daysOfWeekDisabled.indexOf(b.day())!==-1)return!1;if(d.disabledHours&&("h"===c||"m"===c||"s"===c)&&P(b))return!1;if(d.enabledHours&&("h"===c||"m"===c||"s"===c)&&!Q(b))return!1;if(d.disabledTimeIntervals&&("h"===c||"m"===c||"s"===c)){var e=!1;if(a.each(d.disabledTimeIntervals,function(){if(b.isBetween(this[0],this[1]))return e=!0,!1}),e)return!1}return!0},S=function(){for(var b=[],c=f.clone().startOf("y").startOf("d");c.isSame(f,"y");)b.push(a("").attr("data-action","selectMonth").addClass("month").text(c.format("MMM"))),c.add(1,"M");o.find(".datepicker-months td").empty().append(b)},T=function(){var b=o.find(".datepicker-months"),c=b.find("th"),g=b.find("tbody").find("span");c.eq(0).find("span").attr("title",d.tooltips.prevYear),c.eq(1).attr("title",d.tooltips.selectYear),c.eq(2).find("span").attr("title",d.tooltips.nextYear),b.find(".disabled").removeClass("disabled"),R(f.clone().subtract(1,"y"),"y")||c.eq(0).addClass("disabled"),c.eq(1).text(f.year()),R(f.clone().add(1,"y"),"y")||c.eq(2).addClass("disabled"),g.removeClass("active"),e.isSame(f,"y")&&!m&&g.eq(e.month()).addClass("active"),g.each(function(b){R(f.clone().month(b),"M")||a(this).addClass("disabled")})},U=function(){var a=o.find(".datepicker-years"),b=a.find("th"),c=f.clone().subtract(5,"y"),g=f.clone().add(6,"y"),h="";for(b.eq(0).find("span").attr("title",d.tooltips.prevDecade),b.eq(1).attr("title",d.tooltips.selectDecade),b.eq(2).find("span").attr("title",d.tooltips.nextDecade),a.find(".disabled").removeClass("disabled"),d.minDate&&d.minDate.isAfter(c,"y")&&b.eq(0).addClass("disabled"),b.eq(1).text(c.year()+"-"+g.year()),d.maxDate&&d.maxDate.isBefore(g,"y")&&b.eq(2).addClass("disabled");!c.isAfter(g,"y");)h+=''+c.year()+"",c.add(1,"y");a.find("td").html(h)},V=function(){var a,c=o.find(".datepicker-decades"),g=c.find("th"),h=b({y:f.year()-f.year()%100-1}),i=h.clone().add(100,"y"),j=h.clone(),k=!1,l=!1,m="";for(g.eq(0).find("span").attr("title",d.tooltips.prevCentury),g.eq(2).find("span").attr("title",d.tooltips.nextCentury),c.find(".disabled").removeClass("disabled"),(h.isSame(b({y:1900}))||d.minDate&&d.minDate.isAfter(h,"y"))&&g.eq(0).addClass("disabled"),g.eq(1).text(h.year()+"-"+i.year()),(h.isSame(b({y:2e3}))||d.maxDate&&d.maxDate.isBefore(i,"y"))&&g.eq(2).addClass("disabled");!h.isAfter(i,"y");)a=h.year()+12,k=d.minDate&&d.minDate.isAfter(h,"y")&&d.minDate.year()<=a,l=d.maxDate&&d.maxDate.isAfter(h,"y")&&d.maxDate.year()<=a,m+=''+(h.year()+1)+" - "+(h.year()+12)+"",h.add(12,"y");m+="",c.find("td").html(m),g.eq(1).text(j.year()+1+"-"+h.year())},W=function(){var b,c,g,h=o.find(".datepicker-days"),i=h.find("th"),j=[],k=[];if(B()){for(i.eq(0).find("span").attr("title",d.tooltips.prevMonth),i.eq(1).attr("title",d.tooltips.selectMonth),i.eq(2).find("span").attr("title",d.tooltips.nextMonth),h.find(".disabled").removeClass("disabled"),i.eq(1).text(f.format(d.dayViewHeaderFormat)),R(f.clone().subtract(1,"M"),"M")||i.eq(0).addClass("disabled"),R(f.clone().add(1,"M"),"M")||i.eq(2).addClass("disabled"),b=f.clone().startOf("M").startOf("w").startOf("d"),g=0;g<42;g++)0===b.weekday()&&(c=a("
        '+b.week()+"'+b.date()+"
        '+c.format(h?"HH":"hh")+"
        '+c.format("mm")+"
        '+c.format("ss")+"