212 Commits
1.2.1 ... main

Author SHA1 Message Date
耗子
96ff262333 feat: ssh私钥支持设置密码 (#346) 2025-11-11 10:36:19 +08:00
net909
17ffe5704f 1panel支持子节点部署 2025-11-08 16:05:56 +08:00
mrdong916
d4c11b520f 新增Spaceship DNS (#335)
Co-authored-by: mrdong916 <mrdong916@gmail.com>
2025-11-08 16:03:15 +08:00
net909
ba418da84c fix: 证书私钥 EC 指示 2025-11-04 20:42:52 +08:00
TomyJan
b58db855ca fix: 证书私钥 EC 指示 2025-11-04 18:57:55 +08:00
dependabot[bot]
3bd45367b0 Bump symfony/yaml from 7.3.3 to 7.3.5 (#339)
Bumps [symfony/yaml](https://github.com/symfony/yaml) from 7.3.3 to 7.3.5.
- [Release notes](https://github.com/symfony/yaml/releases)
- [Changelog](https://github.com/symfony/yaml/blob/7.3/CHANGELOG.md)
- [Commits](https://github.com/symfony/yaml/compare/v7.3.3...v7.3.5)

---
updated-dependencies:
- dependency-name: symfony/yaml
  dependency-version: 7.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 20:40:10 +08:00
dependabot[bot]
a22bc4fa37 Bump symfony/var-dumper from 7.3.4 to 7.3.5 (#340)
Bumps [symfony/var-dumper](https://github.com/symfony/var-dumper) from 7.3.4 to 7.3.5.
- [Release notes](https://github.com/symfony/var-dumper/releases)
- [Changelog](https://github.com/symfony/var-dumper/blob/7.3/CHANGELOG.md)
- [Commits](https://github.com/symfony/var-dumper/compare/v7.3.4...v7.3.5)

---
updated-dependencies:
- dependency-name: symfony/var-dumper
  dependency-version: 7.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 20:39:46 +08:00
net909
3499583e0d version 2025-10-17 10:41:53 +08:00
耗子
63636e660d JS、CSS本地化 (#332) 2025-10-16 23:09:13 +08:00
net909
4bf87156e3 version 2025-10-10 20:47:20 +08:00
net909
475c14804a 更新composer 2025-10-10 20:11:35 +08:00
net909
531ad68847 Merge branch 'main' of ssh://ssh.github.com:443/netcccyun/dnsmgr 2025-10-10 20:10:54 +08:00
net909
f86c68fc6a 新增K8S部署 2025-10-10 20:10:12 +08:00
dependabot[bot]
460067a5e7 Bump phpmailer/phpmailer from 6.10.0 to 6.11.1 (#324)
Bumps [phpmailer/phpmailer](https://github.com/PHPMailer/PHPMailer) from 6.10.0 to 6.11.1.
- [Release notes](https://github.com/PHPMailer/PHPMailer/releases)
- [Changelog](https://github.com/PHPMailer/PHPMailer/blob/master/changelog.md)
- [Commits](https://github.com/PHPMailer/PHPMailer/compare/v6.10.0...v6.11.1)

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-16 17:08:01 +08:00
Hanada
0837ac9be1 Merge pull request #274 from HanadaLee/multipartpostfix
重构POST内容判断逻辑,去除X-Content-Type标头
2025-07-12 09:44:14 +08:00
net909
c31e0eaf41 修复部署失败 2025-07-11 09:56:08 +08:00
net909
987deda95d 修复报错 2025-07-10 16:41:11 +08:00
消失的彩虹海
654151ce5b Merge pull request #270 from HanadaLee/ssldeployfix
修复部署证书到阿里云和群晖失败的问题
2025-07-08 23:25:03 +08:00
Hanada
2fedee1e93 修复使用guzzle库后部署证书到群晖失败的问题 2025-07-08 23:12:40 +08:00
Hanada
ba97ac3685 修复部署阿里云时证书序列号可能存在前置0的问题 2025-07-08 21:42:40 +08:00
net909
f2f1a0d01e 修复upyun部署 2025-07-07 21:47:15 +08:00
net909
933f98a909 2.8.1 2025-07-06 12:21:04 +08:00
net909
3beb0ec637 修复阿里云WAF部署 2025-07-06 12:19:02 +08:00
net909
7a92ad7094 Merge branch 'main' of ssh://ssh.github.com:443/netcccyun/dnsmgr 2025-07-06 11:17:06 +08:00
net909
5dc4dbfabd 增加Edgeone IP优选,修复EO海外版部署失败 2025-07-06 11:16:44 +08:00
DearTanker
291f715e91 优化记录值复制功能 (#266)
1、修复无法复制包含双引号的 TXT 记录值
2、调整 MX 记录值的复制按钮到右侧,因为复制不需要复制优先级。
2025-07-04 09:43:55 +08:00
net909
4f15d4d7a2 调整快速打开按钮位置 2025-07-03 19:48:34 +08:00
DearTanker
11e83860b7 添加 自定义样式功能、主机记录增加快速打开链接 (#263)
* 添加支持自定义样式功能。

* 在主机记录列中增加一个新窗口打开链接的小按钮,方便快速访问。

* 记录值增加快速复制按钮,并用 padding 来调整了图标的间距避免空格
2025-07-03 19:37:34 +08:00
net909
479af4fe5f 修复批量操作证书和部署任务 2025-06-30 12:44:31 +08:00
net909
3c00e426cc 修改字段长度 2025-06-25 14:35:28 +08:00
net909
814781c1d1 version 2025-06-25 14:24:31 +08:00
net909
af2117faf0 支持雨云SSL自动部署 2025-06-25 13:37:37 +08:00
net909
9275256e36 新增lucky部署,支持直接对接DNS服务商添加域名 2025-06-25 12:57:09 +08:00
net909
d368a0190a 修复API接口操作记录报错 2025-06-23 17:45:47 +08:00
dependabot[bot]
684a4e59ce Bump topthink/think-orm from 4.0.41 to 4.0.44 (#253) 2025-06-20 10:25:54 +08:00
Hanada
1bab48ad93 修复登录设置页面侧边栏没有自动展开的问题 (#252) 2025-06-20 10:21:55 +08:00
耗子
02718e58a9 更新PHPMailer并添加依赖机器人 (#251) 2025-06-20 10:21:40 +08:00
Hanada
8e563a89ff 阿里云ESA支持部署证书到非中国内地 (#249) 2025-06-19 12:51:02 +08:00
Hanada
7f370a095d 修复网宿API验证失败的问题 (#250)
* 变更网宿check方法调用的API接口以避免产品未开通导致验证失败

* 兼容网宿CDN接口使用result作为失败返回值的场景
2025-06-19 12:50:46 +08:00
Hanada
5aab54c79e 将登录验证码开关移到系统设置 (#246) 2025-06-16 19:25:28 +08:00
耗子
ce9ae51aeb 优化错误信息 (#239) 2025-06-10 11:22:52 +08:00
消失的彩虹海
8b06cc5400 Merge pull request #238 from devhaozi/main
恢复自定义解析特性
2025-06-10 10:40:02 +08:00
耗子
a71fd35f6f 恢复自定义解析特性 2025-06-10 10:37:04 +08:00
消失的彩虹海
0d20d8ad46 Merge pull request #236 from devhaozi/main
更新ThinkPHP框架到8.1及curl请求重构
2025-06-10 09:44:52 +08:00
耗子
ff676b7be1 撤销首页提示更改 2025-06-09 14:23:17 +08:00
耗子
8c7c568e5c 撤销首页提示更改 2025-06-09 14:21:42 +08:00
耗子
a4698d67c6 优化函数签名 2025-06-09 05:40:42 +08:00
耗子
5f0e973770 优化函数签名 2025-06-09 05:38:52 +08:00
耗子
a99d0320df 优化函数签名 2025-06-09 05:36:57 +08:00
耗子
bb35ee3378 优化默认Content-Type值及函数名 2025-06-09 05:17:15 +08:00
耗子
8193ade02d 修复get_curl方法header错误 2025-06-09 05:12:06 +08:00
耗子
904c3ceb5b 回滚swoole更改 2025-06-09 05:08:47 +08:00
耗子
5add791ef1 重构使用guzzlehttp发送请求 2025-06-09 05:04:06 +08:00
耗子
3587db2b53 优化代码格式 2025-06-09 01:53:04 +08:00
耗子
4a4cdd059c 优化代码格式 2025-06-09 01:47:53 +08:00
耗子
1021deb60d 优化部分代码 2025-06-09 01:43:58 +08:00
耗子
848610ffe0 优化部分代码 2025-06-09 00:51:50 +08:00
耗子
767aec5ebc 添加框架版本显示 2025-06-09 00:09:48 +08:00
耗子
30912fdf75 更新框架到8.1 2025-06-09 00:01:33 +08:00
net909
5050af2f73 修复阿里云设置权重 2025-06-06 09:49:45 +08:00
net909
6418c3a2ee 支持批量修改部署任务关联证书 2025-06-05 17:03:00 +08:00
net909
efd18676f3 界面优化 2025-06-04 22:52:38 +08:00
net909
5ba7c324af 优化添加部署账户页面 修复部分已知问题 2025-06-03 22:06:02 +08:00
net909
236610d8fb 证书订单支持设置手动续期 2025-05-31 21:34:43 +08:00
net909
0015015b7a 修复lecdn部署 2025-05-31 19:11:40 +08:00
net909
f776b9f47f 支持图形验证码关闭 2025-05-29 17:30:21 +08:00
消失的彩虹海
0860624bd5 Merge pull request #226 from FantajiNeko/main
修复证书申请代理设置,使用反代时不使用代理
2025-05-29 17:01:05 +08:00
Fantajī × Neko
e328dc6808 新增SOCK5H代理类型,远程解析主机名避免DNS污染 2025-05-29 16:43:51 +08:00
Fantajī × Neko
e58d8f4af1 修复证书申请代理设置,使用反代时不使用代理 2025-05-29 15:10:47 +08:00
net909
9d4260062c 修复ucloud证书 2025-05-25 20:44:04 +08:00
net909
17c50e4ba1 修复二级域名申请SSL 2025-05-25 20:35:23 +08:00
net909
55272fd51b 修复自动获取EAB 2025-05-25 12:29:54 +08:00
net909
a1e4476603 Merge branch 'main' of github.com:netcccyun/dnsmgr 2025-05-25 11:46:05 +08:00
net909
3734e98048 新增宝塔云WAF部署 支持修改域名到期时间 2025-05-25 11:45:20 +08:00
消失的彩虹海
372018c03a Merge pull request #218 from devhaozi/main
ZeroSSL和Google EAB支持自动获取及Google反向代理
2025-05-18 22:31:38 +08:00
耗子
9b037834ad 添加反向代理配置参考 2025-05-18 16:39:04 +08:00
耗子
bf05d51d08 修复缩进问题 2025-05-18 16:28:51 +08:00
耗子
ec89fd685b Google EAB支持自动获取及接口反向代理 2025-05-18 16:16:15 +08:00
耗子
300686aa0a ZeroSSL EAB支持自动获取 2025-05-18 14:25:08 +08:00
net909
8a158ea0a5 Merge branch 'main' of github.com:netcccyun/dnsmgr 2025-05-18 10:06:51 +08:00
消失的彩虹海
8a41c1642a Merge pull request #211 from devhaozi/main
支持1Panel v2部署
2025-05-18 10:03:46 +08:00
耗子
6e3350afbd 支持1Panel v2部署 2025-05-16 15:36:27 +08:00
net909
b5f74368d2 Merge branch 'main' of github.com:netcccyun/dnsmgr 2025-05-16 09:33:24 +08:00
消失的彩虹海
49047db438 Merge pull request #209 from devhaozi/main
支持耗子面板部署
2025-05-16 09:32:57 +08:00
耗子
7c54d8af44 修复行分隔符问题 2025-05-16 00:49:29 +08:00
耗子
9b7a7c2d60 支持耗子面板部署 2025-05-16 00:48:11 +08:00
net909
8e7adead48 Merge branch 'main' of github.com:netcccyun/dnsmgr 2025-05-13 10:44:45 +08:00
消失的彩虹海
1e747a8e9e Merge pull request #204 from HanadaLee/main
支持部署证书到网宿CDN、AWS ACM以及修复AWS CloudFront、阿里云OSS部署
2025-05-13 10:39:53 +08:00
Hanada
a13fb38e66 修复AWS CloudFront部署函数中错误的入参 2025-05-13 02:04:28 +08:00
Hanada
3c6944a701 Merge pull request #5 from HanadaLee/wangsudeploy
支持网宿CDN部署
2025-05-13 02:02:43 +08:00
Hanada
c141089c69 支持网宿CDN部署 2025-05-13 02:00:37 +08:00
Hanada
994bdc7fa3 Merge pull request #4 from HanadaLee/aliyunossdeploy
修复部署证书到阿里云OSS失败
2025-05-12 02:30:19 +08:00
Hanada
842b2aa2d9 修复部署证书到阿里云OSS失败 2025-05-12 02:28:13 +08:00
Hanada
4850250f3c Merge pull request #3 from HanadaLee/awsdeploy
支持直接部署证书到AWS ACM
2025-05-12 00:47:11 +08:00
Hanada
f8add88e3d 支持直接部署证书到AWS ACM 2025-05-12 00:43:33 +08:00
Hanada
dcc440c1f9 修复初次上传证书到ACM的逻辑 2025-05-12 00:02:16 +08:00
Hanada
a4d3cdd612 重构证书上传AWS ACM逻辑 2025-05-11 23:40:35 +08:00
Hanada
a483476e6b Merge pull request #2 from HanadaLee/awsdeploy
修复AWS部署失败
2025-05-11 23:18:32 +08:00
Hanada
be55d4b67d Merge pull request #1 from HanadaLee/wangsudeploy
支持网宿CDNPro部署
2025-05-11 23:17:58 +08:00
Hanada
fa47ffb080 修复AWS部署失败 2025-05-11 23:16:15 +08:00
Hanada
333aacaab9 网宿部署增加产品字段,方便后续扩充其他产品线 2025-05-11 21:12:38 +08:00
Hanada
0752f07f7d 支持网宿CDNPro部署
修复spKey逻辑判断问题

支持网宿CDNPro部署
2025-05-11 20:38:47 +08:00
Hanada
4310ccb770 修复CloudFront部署异常 2025-05-10 21:09:12 +08:00
net909
fb8fe3526b 增加证书API接口 2025-05-06 12:16:02 +08:00
net909
cf36b4bd51 部分SSL部署支持批量 2025-05-05 17:37:53 +08:00
net909
079a142b40 新增whois查询域名到期时间+到期提醒 2025-04-28 20:11:33 +08:00
net909
76e9adb405 支持添加的子级域名申请SSL 2025-04-26 20:19:28 +08:00
net909
1723761b94 修改提示 2025-04-07 21:38:47 +08:00
net909
9fb3764878 多个域名批量添加解析、修改解析 2025-04-07 21:26:38 +08:00
net909
651132967f 支持阿里云解析权重修改 2025-04-05 23:03:20 +08:00
net909
81a85fce45 修复部分DNS记录搜索 CF支持暂停解析 2025-04-01 09:47:26 +08:00
net909
4c62084bc6 readme 2025-03-23 12:11:49 +08:00
net909
33ba9a6ebd fix 2025-03-23 11:39:03 +08:00
net909
64d0585788 证书可批量删除、新增支持京东云DNS 2025-03-22 22:28:42 +08:00
net909
1354f63050 version 2025-03-08 09:32:49 +08:00
net909
646fa54bfa fix 2025-03-08 09:32:17 +08:00
net909
2023fb9808 fix 2025-03-07 18:04:34 +08:00
net909
6a45222c1f 支持又拍云部署 2025-03-07 18:04:02 +08:00
net909
44790639cd version 2025-03-05 17:28:51 +08:00
net909
5b12a368fc 新增PowerDNS对接,优化操作日志 2025-03-04 20:49:40 +08:00
net909
36622e6642 新增PVE部署 2025-03-02 14:59:02 +08:00
net909
12d8017df5 增加群辉面板部署,支持状态过滤 2025-02-28 23:04:18 +08:00
消失的彩虹海
70f2e0d487 Merge pull request #150 from devhaozi/main
支持括彩云部署
2025-02-24 22:37:04 +08:00
net909
521275ee33 修复申请中文域名证书,支持批量删域名 2025-02-24 22:34:54 +08:00
耗子
c06bf2d34c 优化括彩云文案 2025-02-24 19:44:40 +08:00
耗子
39dc789ac3 支持括彩云部署 2025-02-24 19:40:37 +08:00
耗子
0877674efb 支持括彩云部署 2025-02-24 19:03:06 +08:00
net909
fe9a50469d 支持宝塔邮局部署 2025-02-13 14:46:49 +08:00
net909
d9f8cc18eb 增加tgbot反代url 2025-02-05 11:48:08 +08:00
net909
48d5ad7569 修改CF优选IP添加解析 2025-02-05 11:17:26 +08:00
net909
8980910d47 增加上次运行时间显示 2025-02-01 10:54:47 +08:00
net909
2ed8a717db 修复namesilo添加解析 2025-02-01 10:46:39 +08:00
net909
b4258dbc81 修复CF解析列表 2025-01-30 10:59:41 +08:00
net909
d1eb6267a2 支持火山引擎更多产品部署 2025-01-24 14:36:29 +08:00
net909
2c81b36249 增加导入已有证书,支持天翼云CDN部署 2025-01-10 14:54:45 +08:00
net909
8d5a9bc083 version 2025-01-05 10:57:43 +08:00
net909
31300d8a7b 优化SSL订单等待耗时 2025-01-05 10:56:02 +08:00
net909
300f2a9b92 修复部分接口请求异常 2024-12-31 15:30:59 +08:00
net909
865275c065 新增西部数码虚拟主机部署 2024-12-30 18:00:47 +08:00
net909
cae9887e05 SSL订单提示优化 2024-12-28 17:17:14 +08:00
net909
6de361171e readme 2024-12-28 17:02:59 +08:00
net909
e59ff8997e 支持批量添加域名 2024-12-28 16:59:48 +08:00
net909
6ce2e006b5 修复泛域名CNAME代理 2024-12-27 22:27:38 +08:00
net909
197d816bbb 修复Goedge账号添加 2024-12-27 13:40:20 +08:00
net909
dfded1122b 云服务商接口支持代理请求 2024-12-26 14:34:55 +08:00
net909
8201318bd7 支持MW面板部署,增加群机器人webhook通知 2024-12-25 20:08:59 +08:00
net909
b31aee7166 fix 2024-12-25 11:37:43 +08:00
net909
06b43fa33f 修复华为云添加TXT解析 2024-12-24 20:41:46 +08:00
net909
fa9235562a 修复华为云部署 2024-12-23 22:31:26 +08:00
net909
92ba34833b 修复1panel 2024-12-22 16:54:34 +08:00
net909
83c1afc186 fix 2024-12-22 11:09:54 +08:00
net909
dcfc545608 fix 2024-12-22 10:07:56 +08:00
net909
17af597022 fix 2024-12-21 23:40:14 +08:00
net909
3fc8c72d1b 修复FTP部署 2024-12-21 23:29:30 +08:00
net909
259693fb3f fix 2024-12-21 19:02:09 +08:00
net909
1e638b5e4c 修复通知 2024-12-21 17:36:13 +08:00
net909
0130613d38 readme 2024-12-21 17:10:21 +08:00
net909
b585e5fa55 SSL证书申请与部署 2024-12-21 17:07:51 +08:00
net909
1ed93cd295 fix 2024-12-19 19:11:32 +08:00
net909
2bcd590ce9 新增支持namesilo 2024-12-19 18:55:05 +08:00
net909
a3b26085a6 格式化代码
增加代理连通性测试
2024-11-09 18:29:20 +08:00
消失的彩虹海
410a33e870 Merge pull request #68 from hoey94/main
添加了docker-compose运行容器的办法
2024-10-25 17:32:35 +08:00
zhaoyihao
f85ac7f724 添加了docker-compose运行容器的办法 2024-10-25 15:35:49 +08:00
net909
86adfd018a update 2024-10-11 20:03:41 +08:00
net909
3b93ee1203 version 2024-09-17 10:22:45 +08:00
net909
a5145accd6 修改CF优选IP地址 2024-09-17 10:21:27 +08:00
net909
30427a9704 更新CF优选IP接口 2024-09-17 10:11:10 +08:00
net909
4145e282f9 fix 2024-08-30 14:41:29 +08:00
net909
53ec0e12df 修复高版本php 2024-08-28 16:51:51 +08:00
net909
b8addd6d27 新增百度云、火山引擎 2024-08-24 19:42:31 +08:00
net909
0d1d7d3c67 update 2024-08-07 14:05:44 +08:00
net909
8bdf58651c version 2024-08-05 17:10:43 +08:00
net909
350a0bd306 fix 2024-07-25 22:37:41 +08:00
net909
ab074da839 fix 2024-07-25 20:39:21 +08:00
net909
57a237f898 1.6 2024-07-07 15:54:11 +08:00
net909
bf6a091c8d version 2024-07-07 13:39:52 +08:00
net909
14700052b8 修复页面加载错误,支持dnspod国际版 2024-07-07 13:39:27 +08:00
net909
f43f2b196d fix 2024-06-09 10:11:00 +08:00
net909
949b52722a readme 2024-06-08 12:19:20 +08:00
net909
1c504f3b04 解析支持高级搜索 2024-06-07 18:25:31 +08:00
net909
8e4fd32f8c fix 2024-05-24 15:11:28 +08:00
net909
5cd6b108f0 修改syskey存储 2024-05-23 16:26:40 +08:00
net909
424d2c8132 sql 2024-05-22 11:25:07 +08:00
net909
86bffab357 1.4 2024-05-22 11:24:13 +08:00
net909
c1edf905f3 优化解析记录分页 2024-05-22 11:17:34 +08:00
net909
b252816711 支持批量添加修改解析 2024-05-17 18:35:56 +08:00
net909
7a58292fb9 批量修改与添加 2024-05-10 19:43:52 +08:00
net909
d4b7bcfbf6 fix 2024-05-01 17:57:34 +08:00
net909
f20b132d97 新增CF优选IP 2024-05-01 10:51:34 +08:00
net909
ad85fa7c22 修复 2024-04-26 23:16:39 +08:00
268 changed files with 42660 additions and 7862 deletions

View File

@@ -1,19 +1,18 @@
APP_DEBUG = false
[APP]
DEFAULT_TIMEZONE = Asia/Shanghai
SYS_KEY = {syskey}
[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

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

15
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# Dependabot configuration.
#
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/working-with-dependabot/dependabot-options-reference
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "daily"

1
.gitignore vendored
View File

@@ -3,4 +3,3 @@
/vendor
*.log
.env
/composer.lock

294
README.md
View File

@@ -1,85 +1,209 @@
## 聚合DNS管理系统
聚合DNS管理系统可以实现在一个网站内管理多个平台的域名解析目前已支持的域名平台有
- 阿里云
- 腾讯云
- 华为云
- 西部数码
- DNSLA
- CloudFlare
本系统支持多用户每个用户可分配不同的域名解析权限支持API接口支持获取域名独立DNS控制面板登录链接方便各种IDC系统对接。
### 演示截图
添加域名账户
![](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)
### 部署方法
* 从[Release](https://github.com/netcccyun/dnsmgr/releases)页面下载安装包
* 运行环境要求PHP7.4+MySQL5.6+
* 设置网站运行目录为`public`
* 设置伪静态为`ThinkPHP`
* 如果是下载的Source code包还需Composer安装依赖Release页面下载的安装包不需要
```
composer install --no-dev
```
* 访问网站,会自动跳转到安装页面,根据提示安装完成
* 访问首页登录控制面板
##### 伪静态规则
* Nginx
```
location / {
if (!-e $request_filename){
rewrite ^(.*)$ /index.php?s=$1 last; break;
}
}
```
* Apache
```
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
</IfModule>
```
### 版权信息
版权所有Copyright © 2023~2024 by 消失的彩虹海(https://blog.cccyun.cn)
### 其他推荐
- [彩虹云主机 - 免备案CDN/虚拟主机](https://www.cccyun.net/)
- [小白云高防云服务器](https://www.xiaobaiyun.cn/aff/GMLPMFOV)
# 彩虹聚合DNS管理系统
<div align="center">
[![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)
</div>
彩虹聚合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
```
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
</IfModule>
```
### 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
🤝 捐赠:
<img height="240" src="https://wkphoto.bj.bcebos.com/d8f9d72a6059252db065f556249b033b5bb5b976.jpg">
### 其他推荐
- [彩虹云主机 - 免备案CDN/虚拟主机](https://www.cccyun.net/)
- [小白云高防云服务器](https://www.xiaobaiyun.cn/aff/GMLPMFOV)

View File

@@ -102,15 +102,17 @@ abstract class BaseController
if ($url) {
$url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : (string)$this->app->route->buildUrl($url);
}
if(empty($msg)) $msg = '未知错误';
if (empty($msg)) {
$msg = '未知错误';
}
if (request()->isApi) {
return json(['code'=>$code=='success'?0:-1, 'msg'=>$msg]);
if ($this->request->isApi) {
return json(['code' => $code == 'success' ? 0 : -1, 'msg' => $msg]);
}
if (request()->isAjax()) {
return json(['code'=>$code=='success'?0:-1, 'msg'=>$msg, 'url'=>$url]);
if ($this->request->isAjax()) {
return json(['code' => $code == 'success' ? 0 : -1, 'msg' => $msg, 'url' => $url]);
}
View::assign([
'code' => $code,
'msg' => $msg,

42
app/command/Certtask.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace app\command;
use Exception;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Config;
use app\service\OptimizeService;
use app\service\CertTaskService;
use app\service\ExpireNoticeService;
use app\service\ScheduleService;
class Certtask extends Command
{
protected function configure()
{
// 指令配置
$this->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;
}
}

View File

@@ -1,76 +1,82 @@
<?php
declare (strict_types = 1);
namespace app\command;
use Exception;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use app\lib\TaskRunner;
class Dmtask extends Command
{
protected function configure()
{
// 指令配置
$this->setName('dmtask')
->setDescription('容灾切换任务');
}
protected function execute(Input $input, Output $output)
{
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);
}
});
}
}
<?php
declare(strict_types=1);
namespace app\command;
use Exception;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Config;
use app\service\TaskRunner;
class Dmtask extends Command
{
protected function configure()
{
// 指令配置
$this->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);
}
});
}
}

47
app/command/Reset.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace app\command;
use Exception;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Config;
class Reset extends Command
{
protected function configure()
{
// 指令配置
$this->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关闭成功');
}
}
}

View File

@@ -1,228 +1,265 @@
<?php
// 应用公共文件
use think\facade\Db;
use think\facade\Request;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
function get_curl($url, $post=0, $referer=0, $cookie=0, $header=0, $ua=0, $nobody=0, $addheader=0)
function get_curl($url, $post = 0, $referer = 0, $cookie = 0, $ua = 0, $nobody = 0, $addheader = [])
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
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";
if($addheader){
$httpheader = array_merge($httpheader, $addheader);
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
if ($post) {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
if ($header) {
curl_setopt($ch, CURLOPT_HEADER, true);
}
if ($cookie) {
curl_setopt($ch, CURLOPT_COOKIE, $cookie);
}
if($referer){
curl_setopt($ch, CURLOPT_REFERER, $referer);
}
if ($ua) {
curl_setopt($ch, CURLOPT_USERAGENT, $ua);
}
else {
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");
}
if ($nobody) {
curl_setopt($ch, CURLOPT_NOBODY, 1);
}
curl_setopt($ch, CURLOPT_ENCODING, "gzip");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$ret = curl_exec($ch);
curl_close($ch);
return $ret;
$options = [
'timeout' => 10,
'verify' => false,
'headers' => [
'User-Agent' => $ua ?: '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 // 不抛出异常
];
$options['headers'] = array_merge($options['headers'], $addheader);
if ($referer) {
$options['headers']['Referer'] = $referer;
}
if ($cookie) {
$options['headers']['Cookie'] = $cookie;
}
$method = 'GET';
if ($post) {
$method = 'POST';
if (!isset($options['headers']['Content-Type'])) {
$options['headers']['Content-Type'] = 'application/x-www-form-urlencoded';
}
if (is_array($post)) {
$options['form_params'] = $post;
} else {
$options['body'] = $post;
}
}
try {
$client = new Client();
$response = $client->request($method, $url, $options);
if ($nobody) {
return '';
}
return $response->getBody()->getContents();
} catch (GuzzleException $e) {
return '';
}
}
function real_ip($type=0){
function real_ip($type = 0)
{
$ip = $_SERVER['REMOTE_ADDR'];
if($type<=0 && isset($_SERVER['HTTP_X_FORWARDED_FOR']) && preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] AS $xip) {
if ($type <= 0 && isset($_SERVER['HTTP_X_FORWARDED_FOR']) && preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] as $xip) {
if (filter_var($xip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$ip = $xip;
break;
}
}
} elseif ($type<=0 && isset($_SERVER['HTTP_CLIENT_IP']) && filter_var($_SERVER['HTTP_CLIENT_IP'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
} elseif ($type <= 0 && isset($_SERVER['HTTP_CLIENT_IP']) && filter_var($_SERVER['HTTP_CLIENT_IP'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif ($type<=1 && isset($_SERVER['HTTP_CF_CONNECTING_IP']) && filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
} elseif ($type <= 1 && isset($_SERVER['HTTP_CF_CONNECTING_IP']) && filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
} elseif ($type<=1 && isset($_SERVER['HTTP_X_REAL_IP']) && filter_var($_SERVER['HTTP_X_REAL_IP'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
} elseif ($type <= 1 && isset($_SERVER['HTTP_X_REAL_IP']) && filter_var($_SERVER['HTTP_X_REAL_IP'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
}
return $ip;
}
function strexists($string, $find) {
return !(strpos($string, $find) === FALSE);
function strexists($string, $find)
{
return !(strpos($string, $find) === FALSE);
}
function dstrpos($string, $arr) {
if(empty($string)) return false;
foreach((array)$arr as $v) {
if(strpos($string, $v) !== false) {
return true;
}
}
return false;
function dstrpos($string, $arr)
{
if (empty($string)) return false;
foreach ((array)$arr as $v) {
if (strpos($string, $v) !== false) {
return true;
}
}
return false;
}
function checkmobile() {
$useragent = strtolower($_SERVER['HTTP_USER_AGENT']);
$ualist = array('android', 'midp', 'nokia', 'mobile', 'iphone', 'ipod', 'blackberry', 'windows phone');
if((dstrpos($useragent, $ualist) || strexists($_SERVER['HTTP_ACCEPT'], "VND.WAP") || strexists($_SERVER['HTTP_VIA'],"wap")))
return true;
else
return false;
function checkmobile()
{
$useragent = strtolower($_SERVER['HTTP_USER_AGENT']);
$ualist = array('android', 'midp', 'nokia', 'mobile', 'iphone', 'ipod', 'blackberry', 'windows phone');
if ((dstrpos($useragent, $ualist) || strexists($_SERVER['HTTP_ACCEPT'], "VND.WAP") || strexists($_SERVER['HTTP_VIA'], "wap"))) {
return true;
} else {
return false;
}
}
function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {
$ckey_length = 4;
$key = md5($key);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya.md5($keya.$keyc);
$key_length = strlen($cryptkey);
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
if($operation == 'DECODE') {
if(((int)substr($result, 0, 10) == 0 || (int)substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
return substr($result, 26);
} else {
return '';
}
} else {
return $keyc.base64_encode($result);
}
function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0)
{
$ckey_length = 4;
$key = md5($key);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya.md5($keya.$keyc);
$key_length = strlen($cryptkey);
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for ($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for ($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
if ($operation == 'DECODE') {
if (((int)substr($result, 0, 10) == 0 || (int)substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
return substr($result, 26);
} else {
return '';
}
} else {
return $keyc.base64_encode($result);
}
}
function random($length, $numeric = 0) {
$seed = base_convert(md5(microtime().$_SERVER['DOCUMENT_ROOT']), 16, $numeric ? 10 : 35);
$seed = $numeric ? (str_replace('0', '', $seed).'012340567890') : ($seed.'zZ'.strtoupper($seed));
$hash = '';
$max = strlen($seed) - 1;
for($i = 0; $i < $length; $i++) {
$hash .= $seed[mt_rand(0, $max)];
}
return $hash;
function random($length, $numeric = 0)
{
$seed = base_convert(md5(microtime().$_SERVER['DOCUMENT_ROOT']), 16, $numeric ? 10 : 35);
$seed = $numeric ? (str_replace('0', '', $seed).'012340567890') : ($seed.'zZ'.strtoupper($seed));
$hash = '';
$max = strlen($seed) - 1;
for ($i = 0; $i < $length; $i++) {
$hash .= $seed[mt_rand(0, $max)];
}
return $hash;
}
function checkDomain($domain){
if(empty($domain) || !preg_match('/^[-$a-z0-9_*.]{2,512}$/i', $domain) || (stripos($domain, '.') === false) || substr($domain, -1) == '.' || substr($domain, 0 ,1) == '.' || substr($domain, 0 ,1) == '*' && substr($domain, 1 ,1) != '.' || substr_count($domain, '*')>1 || strpos($domain, '*')>0 || strlen($domain)<4) return false;
return true;
function checkDomain($domain)
{
if (empty($domain) || !preg_match('/^[-$a-z0-9_*.]{2,512}$/i', $domain) || (stripos($domain, '.') === false) || substr($domain, -1) == '.' || substr($domain, 0, 1) == '.' || substr($domain, 0, 1) == '*' && substr($domain, 1, 1) != '.' || substr_count($domain, '*') > 1 || strpos($domain, '*') > 0 || strlen($domain) < 4) return false;
return true;
}
function getSubstr($str, $leftStr, $rightStr)
{
$left = strpos($str, $leftStr);
$start = $left+strlen($leftStr);
$right = strpos($str, $rightStr, $start);
if($left < 0) return '';
if($right>0){
return substr($str, $start, $right-$start);
}else{
return substr($str, $start);
}
$left = strpos($str, $leftStr);
$start = $left + strlen($leftStr);
$right = strpos($str, $rightStr, $start);
if ($left < 0) return '';
if ($right > 0) {
return substr($str, $start, $right - $start);
} else {
return substr($str, $start);
}
}
function checkRefererHost(){
if(!request()->header('referer'))return false;
$url_arr = parse_url(request()->header('referer'));
$http_host = request()->header('host');
if(strpos($http_host,':'))$http_host = substr($http_host, 0, strpos($http_host, ':'));
function arrays_are_equal($array1, $array2)
{
return empty(array_diff($array1, $array2)) && empty(array_diff($array2, $array1));
}
function checkRefererHost()
{
if (!Request::header('referer')) {
return false;
}
$url_arr = parse_url(Request::header('referer'));
$http_host = Request::header('host');
if (strpos($http_host, ':')) {
$http_host = substr($http_host, 0, strpos($http_host, ':'));
}
return $url_arr['host'] === $http_host;
}
function checkIfActive($string) {
$array=explode(',',$string);
$action = request()->action();
if (in_array($action,$array)){
return 'active';
}else
return null;
function checkIfActive($string)
{
$array = explode(',', $string);
$action = Request::action();
if (in_array($action, $array)) {
return 'active';
} else {
return null;
}
}
function getSid() {
function getSid()
{
return md5(uniqid(mt_rand(), true) . microtime());
}
function getMd5Pwd($pwd, $salt=null) {
function getMd5Pwd($pwd, $salt = null)
{
return md5(md5($pwd) . md5('1277180438'.$salt));
}
function isNullOrEmpty($str){
return $str === null || $str === '';
function isNullOrEmpty($str)
{
return $str === null || $str === '';
}
function checkPermission($type, $domain = null){
$user = request()->user;
if(empty($user)) return false;
if($user['level'] == 2) return true;
if($type == 1 && $user['level'] == 1 || $type == 0 && $user['level'] >= 0){
if($domain == null) return true;
if(in_array($domain, $user['permission'])){
return true;
}
}
return false;
function checkPermission($type, $domain = null)
{
$user = Request()->user;
if (empty($user)) {
return false;
}
if ($user['level'] == 2) {
return true;
}
if ($type == 1 && $user['level'] == 1 || $type == 0 && $user['level'] >= 0) {
if ($domain == null) {
return true;
}
if (in_array($domain, $user['permission'])) {
return true;
}
}
return false;
}
function getAdminSkin(){
$skin = cookie('admin_skin');
if(empty($skin)){
$skin = config_get('admin_skin');
}
if(empty($skin)){
$skin = 'skin-black-blue';
}
return $skin;
function getAdminSkin()
{
$skin = cookie('admin_skin');
if (empty($skin)) {
$skin = config_get('admin_skin');
}
if (empty($skin)) {
$skin = 'skin-black-blue';
}
return $skin;
}
function config_get($key, $default = null, $force = false)
{
if ($force) {
$value = Db::name('config')->where('key', $key)->value('value');
} else {
$value = config('sys.'.$key);
}
if ($force) {
$value = Db::name('config')->where('key', $key)->value('value');
} else {
$value = config('sys.' . $key);
}
return $value ?: $default;
}
function config_set($key, $value)
{
$res = Db::name('config')->replace()->insert(['key'=>$key, 'value'=>$value]);
return $res!==false;
$res = Db::name('config')->replace()->insert(['key' => $key, 'value' => $value]);
return $res !== false;
}
function getMillisecond()
@@ -231,23 +268,340 @@ function getMillisecond()
return (int)sprintf('%.0f', (floatval($s1) + floatval($s2)) * 1000);
}
function getDnsType($value){
if(filter_var($value, FILTER_VALIDATE_IP))return 'A';
else return 'CNAME';
function getDnsType($value)
{
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return 'A';
} elseif (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return 'AAAA';
} else {
return 'CNAME';
}
}
function convert_second($s){
$m = floor($s/60);
if($m == 0){
return $s.'秒';
}else{
$s = $s%60;
$h = floor($m/60);
if($h == 0){
return $m.'分钟'.$s.'秒';
}else{
$m = $m%60;
return $h.'小时'.$m.'分钟'.$s.'秒';
}
}
}
function convert_second($s)
{
$m = floor($s / 60);
if ($m == 0) {
return $s.'秒';
} else {
$s = $s % 60;
$h = floor($m / 60);
if ($h == 0) {
return $m.'分钟'.$s.'秒';
} else {
$m = $m % 60;
return $h.'小时'.$m.'分钟'.$s.'秒';
}
}
}
function getMainDomain($host)
{
if (filter_var($host, FILTER_VALIDATE_IP)) return $host;
$domains = config('temp.domains');
if (!$domains) {
$domains = Db::name('domain')->column('name');
config(['domains'=>$domains], 'temp');
}
foreach ($domains as $domain) {
if (str_ends_with($host, $domain)) {
return $domain;
}
}
$domain_root = file_get_contents(app()->getBasePath() . 'data' . DIRECTORY_SEPARATOR . 'domain_root.txt');
$domain_root = explode("\r\n", $domain_root);
$data = explode('.', $host);
$co_ta = count($data);
if ($co_ta <= 2) return $host;
$domain_name = $data[$co_ta - 2] . '.' . $data[$co_ta - 1];
if (in_array($domain_name, $domain_root) && $co_ta > 2) {
$domain_name = $data[$co_ta - 3] . '.' . $domain_name;
}
return $domain_name;
}
function check_proxy($url, $proxy_server, $proxy_port, $type, $proxy_user, $proxy_pwd)
{
match ($type) {
'https' => $proxy_string = 'https://',
'sock4' => $proxy_string = 'socks4://',
'sock5' => $proxy_string = 'socks5://',
'sock5h' => $proxy_string = 'socks5h://',
default => $proxy_string = 'http://',
};
if (!empty($proxy_user) && !empty($proxy_pwd)) {
$proxy_string .= $proxy_user . ':' . $proxy_pwd . '@';
}
$proxy_string .= $proxy_server . ':' . intval($proxy_port);
$options = [
'proxy' => $proxy_string,
'timeout' => 3,
'connect_timeout' => 3,
'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'
]
];
try {
$client = new Client();
$response = $client->request('GET', $url, $options);
$httpCode = $response->getStatusCode();
if ($httpCode >= 200 && $httpCode < 400) {
return true;
} else {
throw new Exception('HTTP状态码异常' . $httpCode);
}
} catch (GuzzleException $e) {
throw new Exception(guzzle_error($e));
}
}
function clearDirectory($dir): bool
{
// 确保路径是目录
if (!is_dir($dir)) {
return false;
}
// 打开目录
$items = scandir($dir);
foreach ($items as $item) {
// 跳过 '.' 和 '..'
if ($item == '.' || $item == '..') {
continue;
}
// 完整路径
$path = $dir . DIRECTORY_SEPARATOR . $item;
// 如果是目录,递归删除其内容
if (is_dir($path)) {
clearDirectory($path);
// 删除空目录
rmdir($path);
} else {
// 删除文件
unlink($path);
}
}
return true;
}
/**
* 发送 HTTP 请求
*
* @param string $url 请求URL
* @param mixed $data 请求数据,可以是字符串或数组,数组将自动根据请求方法及传入的 Content-Type 头序列化
* @param string|null $referer 请求的 Referer 头
* @param array|null $cookie 请求的 Cookie 头
* @param array|null $headers 其他自定义请求头
* @param bool $proxy 是否使用代理
* @param string|null $method 请求方法,默认为 GET 或 POST如果存在请求数据
* @param int $timeout 请求超时时间,默认为 10 秒
* @return array 包含 HTTP 状态码、重定向 URL、响应头和响应体的数组
* @throws Exception
*/
function http_request($url, $data = null, $referer = null, $cookie = null, $headers = null, $proxy = false, $method = null, $timeout = 10): array
{
$options = [
'timeout' => $timeout,
'connect_timeout' => $timeout,
'allow_redirects' => false,
'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 (!$method) {
$method = $data ? 'POST' : 'GET';
}
// 处理头部
if (is_array($headers)) {
$options['headers'] = array_merge($options['headers'], $headers);
}
// 处理Cookie
if ($cookie) {
$options['headers']['Cookie'] = $cookie;
}
// 处理Referer
if ($referer) {
$options['headers']['Referer'] = $referer;
}
// 处理数据
if ($data) {
if ($method !== 'GET') {
if (is_string($data)) {
$options['body'] = $data;
if (!isset($options['headers']['Content-Type'])) {
if (json_validate($data)) {
// json
$options['headers']['Content-Type'] = 'application/json';
} elseif (str_contains($data, '=') || str_contains($data, '&')) {
// 表单
$options['headers']['Content-Type'] = 'application/x-www-form-urlencoded';
}
}
} else if (is_array($data) || is_object($data)) {
if (!isset($options['headers']['Content-Type'])) {
// 默认为表单
$options['headers']['Content-Type'] = 'application/x-www-form-urlencoded';
}
if ($options['headers']['Content-Type'] == 'application/x-www-form-urlencoded') {
// 表单
$options['form_params'] = $data;
} else if ($options['headers']['Content-Type'] == 'multipart/form-data') {
// 表单文件
$options['multipart'] = $data;
unset($options['headers']['Content-Type']); // 由GuzzleHttp重新生成Content-Type头部
} else if ($options['headers']['Content-Type'] == 'application/json') {
// json
$options['json'] = $data;
} else {
// 其他
$options['body'] = http_build_query($data);
}
} else {
$options['body'] = $data;
}
} else {
// 兼容已经存在查询字符串的情况
if (!str_contains($url, '?')) {
$options['query'] = $data;
}
}
}
// 处理代理
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)) {
throw new Exception('代理服务器或端口未配置');
}
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($method, $url, $options);
$code = $response->getStatusCode();
// 取重定向URL
$redirect_url = '';
if ($code >= 300 && $code < 400) {
$redirect_url = $response->getHeaderLine('Location');
}
return [
'code' => $code,
'redirect_url' => $redirect_url,
'headers' => $response->getHeaders(),
'body' => $response->getBody()->getContents()
];
} catch (GuzzleException $e) {
throw new Exception('请求失败: ' . guzzle_error($e));
}
}
function guzzle_error($e)
{
$errmsg = $e->getMessage();
if (preg_match('/^cURL error \d+: /', $errmsg)) {
$errmsg = preg_replace('/^cURL error \d+: /', '', $errmsg);
}
$pos = strpos($errmsg, ' (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)');
if ($pos !== false) {
$errmsg = substr($errmsg, 0, $pos);
}
if (strlen($errmsg) > 100) {
$errmsg = substr($errmsg, 0, 97) . '...';
}
return $errmsg;
}
function curl_set_proxy(&$ch)
{
$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)) {
return;
}
if ($proxy_type == 'https') {
$proxy_type = CURLPROXY_HTTPS;
} elseif ($proxy_type == 'sock4') {
$proxy_type = CURLPROXY_SOCKS4;
} elseif ($proxy_type == 'sock5') {
$proxy_type = CURLPROXY_SOCKS5;
} elseif ($proxy_type == 'sock5h') {
$proxy_type = CURLPROXY_SOCKS5_HOSTNAME;
} else {
$proxy_type = CURLPROXY_HTTP;
}
curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_PROXY, $proxy_server);
curl_setopt($ch, CURLOPT_PROXYPORT, $proxy_port);
if ($proxy_userpwd != ':') {
curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxy_userpwd);
}
curl_setopt($ch, CURLOPT_PROXYTYPE, $proxy_type);
}
function convertDomainToAscii($domain) {
if (preg_match('/[\x{4e00}-\x{9fa5}]/u', $domain)) {
return idn_to_ascii($domain);
} else {
return $domain;
}
}
function convertDomainToUtf8($domain) {
if (preg_match('/^xn--/', $domain)) {
return idn_to_utf8($domain);
} else {
return $domain;
}
}
function getDomainDate($domain)
{
try {
$whois = \Iodev\Whois\Factory::get()->createWhois();
$info = $whois->loadDomainInfo($domain);
if ($info) {
if ($info->expirationDate > 0) {
return [$info->creationDate > 0 ? date('Y-m-d H:i:s', $info->creationDate) : null, date('Y-m-d H:i:s', $info->expirationDate)];
} else {
throw new Exception('域名到期时间未知');
}
} else {
throw new Exception('域名信息未找到');
}
} catch (Exception $e) {
throw new Exception('查询域名whois失败: ' . $e->getMessage());
}
}

View File

@@ -1,58 +1,61 @@
<?php
namespace app\controller;
use app\BaseController;
use Exception;
use think\facade\Db;
use think\facade\View;
use app\lib\DnsHelper;
class Auth extends BaseController
{
public function verifycode()
public function login()
{
return captcha();
}
$login_limit_count = 5; //登录失败次数
$login_limit_file = app()->getRuntimePath() . '@login.lock';
public function login(){
$login_limit_count = 5;//登录失败次数
$login_limit_file = app()->getRuntimePath().'@login.lock';
if(request()->islogin){
if ($this->request->islogin) {
return redirect('/');
}
if(request()->isAjax()){
$username = input('post.username',null,'trim');
$password = input('post.password',null,'trim');
$code = input('post.code',null,'trim');
if(empty($username) || empty($password)){
return json(['code'=>-1, 'msg'=>'用户名或密码不能为空']);
if ($this->request->isAjax()) {
$username = input('post.username', null, 'trim');
$password = input('post.password', null, 'trim');
$code = input('post.code', null, 'trim');
if (empty($username) || empty($password)) {
return json(['code' => -1, 'msg' => '用户名或密码不能为空']);
}
if(!captcha_check($code)){
return json(['code'=>-1, 'msg'=>'验证码错误', 'vcode'=>1]);
if (config_get('vcode', '1') == '1' && !captcha_check($code)) {
return json(['code' => -1, 'msg' => '验证码错误', 'vcode' => 1]);
}
if (file_exists($login_limit_file)) {
$login_limit = unserialize(file_get_contents($login_limit_file));
if ($login_limit['count'] >= $login_limit_count && $login_limit['time'] > time() - 86400) {
exit(json_encode(['code' => -1, 'msg' => '多次登录失败,暂时禁止登录。可删除/runtime/@login.lock文件解除限制', 'vcode'=>1]));
if ($login_limit['count'] >= $login_limit_count && $login_limit['time'] > time() - 7200) {
return json(['code' => -1, 'msg' => '多次登录失败,暂时禁止登录。可删除/runtime/@login.lock文件解除限制', 'vcode' => 1]);
}
}
$user = Db::name('user')->where('username', $username)->find();
if($user && password_verify($password, $user['password'])){
if($user['status'] == 0) return json(['code'=>-1, 'msg'=>'此用户已被封禁', 'vcode'=>1]);
Db::name('log')->insert(['uid' => $user['id'], 'action' => '登录后台', 'data' => 'IP:'.$this->clientip, 'addtime' => date("Y-m-d H:i:s")]);
DB::name('user')->where('id', $user['id'])->update(['lasttime' => date("Y-m-d H:i:s")]);
$session = md5($user['id'].$user['password']);
$expiretime = time()+2562000;
$token = authcode("user\t{$user['id']}\t{$session}\t{$expiretime}", 'ENCODE', env('app.sys_key'));
cookie('user_token', $token, ['expire' => $expiretime, 'httponly' => true]);
return json(['code'=>0]);
}else{
if($user){
Db::name('log')->insert(['uid' => $user['id'], 'action' => '登录失败', 'data' => 'IP:'.$this->clientip, 'addtime' => date("Y-m-d H:i:s")]);
if ($user && password_verify($password, $user['password'])) {
if ($user['status'] == 0) return json(['code' => -1, 'msg' => '此用户已被封禁', 'vcode' => 1]);
if (isset($user['totp_open']) && $user['totp_open'] == 1 && !empty($user['totp_secret'])) {
session('pre_login_user', $user['id']);
if (file_exists($login_limit_file)) {
unlink($login_limit_file);
}
return json(['code' => -1, 'msg' => '需要验证动态口令', 'vcode' => 2]);
}
$this->loginUser($user);
if (file_exists($login_limit_file)) {
unlink($login_limit_file);
}
return json(['code' => 0]);
} else {
if ($user) {
Db::name('log')->insert(['uid' => $user['id'], 'action' => '登录失败', 'data' => 'IP:' . $this->clientip, 'addtime' => date("Y-m-d H:i:s")]);
if (isset($user['totp_open']) && $user['totp_open'] == 1 && !empty($user['totp_secret'])) {
return json(['code' => -1, 'msg' => '用户名或密码错误', 'vcode' => 1]);
}
}
if (!file_exists($login_limit_file)) {
$login_limit = ['count' => 0, 'time' => 0];
@@ -72,6 +75,28 @@ class Auth extends BaseController
return view();
}
public function totp()
{
$uid = session('pre_login_user');
if (empty($uid)) return json(['code' => -1, 'msg' => '请重新登录']);
$code = input('post.code');
if (empty($code)) return json(['code' => -1, 'msg' => '请输入动态口令']);
$user = Db::name('user')->where('id', $uid)->find();
if (!$user) return json(['code' => -1, 'msg' => '用户不存在']);
if ($user['totp_open'] == 0 || empty($user['totp_secret'])) return json(['code' => -1, 'msg' => '未开启TOTP二次验证']);
try {
$totp = \app\lib\TOTP::create($user['totp_secret']);
if (!$totp->verify($code)) {
return json(['code' => -1, 'msg' => '动态口令错误']);
}
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
$this->loginUser($user);
session('pre_login_user', null);
return json(['code' => 0]);
}
public function logout()
{
cookie('user_token', null);
@@ -80,36 +105,55 @@ class Auth extends BaseController
public function quicklogin()
{
$domain = input('get.domain',null,'trim');
$timestamp = input('get.timestamp',null,'trim');
$token = input('get.token',null,'trim');
$sign = input('get.sign',null,'trim');
if(empty($domain) || empty($timestamp) || empty($token) || empty($sign)){
$domain = input('get.domain', null, 'trim');
$timestamp = input('get.timestamp', null, 'trim');
$token = input('get.token', null, 'trim');
$sign = input('get.sign', null, 'trim');
if (empty($domain) || empty($timestamp) || empty($token) || empty($sign)) {
return $this->alert('error', '参数错误');
}
if($timestamp < time()-300 || $timestamp > time()+300){
if ($timestamp < time() - 300 || $timestamp > time() + 300) {
return $this->alert('error', '时间戳无效');
}
if(md5(env('app.sys_key').$domain.$timestamp.$token.env('app.sys_key')) !== $sign){
if (md5(config_get('sys_key') . $domain . $timestamp . $token . config_get('sys_key')) !== $sign) {
return $this->alert('error', '签名错误');
}
if($token != cache('quicklogin_'.$domain)){
if ($token != cache('quicklogin_' . $domain)) {
return $this->alert('error', 'Token无效');
}
$row = Db::name('domain')->where('name', $domain)->find();
if(!$row){
if (!$row) {
return $this->alert('error', '该域名不存在');
}
if(!$row['is_sso']){
if (!$row['is_sso']) {
return $this->alert('error', '该域名不支持快捷登录');
}
Db::name('log')->insert(['uid' => 0, 'action' => '域名快捷登录', 'data' => 'IP:'.$this->clientip, 'addtime' => date("Y-m-d H:i:s"), 'domain' => $domain]);
$this->loginDomain($row);
return redirect('/record/' . $row['id']);
}
$session = md5($row['id'].$row['name']);
$expiretime = time()+2562000;
$token = authcode("domain\t{$row['id']}\t{$session}\t{$expiretime}", 'ENCODE', env('app.sys_key'));
private function loginUser($user)
{
Db::name('log')->insert(['uid' => $user['id'], 'action' => '登录后台', 'data' => 'IP:' . $this->clientip, 'addtime' => date("Y-m-d H:i:s")]);
DB::name('user')->where('id', $user['id'])->update(['lasttime' => date("Y-m-d H:i:s")]);
$session = md5($user['id'] . $user['password']);
$expiretime = time() + 2562000;
$token = authcode("user\t{$user['id']}\t{$session}\t{$expiretime}", 'ENCODE', config_get('sys_key'));
cookie('user_token', $token, ['expire' => $expiretime, 'httponly' => true]);
return redirect('/record/'.$row['id']);
}
private function loginDomain($row)
{
Db::name('log')->insert(['uid' => 0, 'action' => '域名快捷登录', 'data' => 'IP:' . $this->clientip, 'addtime' => date("Y-m-d H:i:s"), 'domain' => $row['name']]);
$session = md5($row['id'] . $row['name']);
$expiretime = time() + 2562000;
$token = authcode("domain\t{$row['id']}\t{$session}\t{$expiretime}", 'ENCODE', config_get('sys_key'));
cookie('user_token', $token, ['expire' => $expiretime, 'httponly' => true]);
}
public function verifycode()
{
return captcha();
}
}

970
app/controller/Cert.php Normal file
View File

@@ -0,0 +1,970 @@
<?php
namespace app\controller;
use app\BaseController;
use app\lib\CertHelper;
use app\lib\DeployHelper;
use app\service\CertOrderService;
use app\service\CertDeployService;
use Exception;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
class Cert extends BaseController
{
public function certaccount()
{
if (!checkPermission(2)) return $this->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' => '该证书已存在,无需重复添加']);
}
}
}
if ($certInfo['keytype'] == 'ECC') {
$privatekey = CertHelper::ensureECPrivateKeyFormat($privatekey);
}
$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'];
if ($certInfo['keytype'] == 'ECC') {
$privatekey = CertHelper::ensureECPrivateKeyFormat($privatekey);
}
$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();
}
}

View File

@@ -1,265 +1,266 @@
<?php
namespace app\controller;
use app\BaseController;
use Exception;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
use app\lib\DnsHelper;
class Dmonitor extends BaseController
{
public function overview()
{
if(!checkPermission(2)) return $this->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') ? '<font color="green">已安装</font>' : '<font color="red">未安装</font>',
]);
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);
$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.'%');
}
}
$total = $select->count();
$list = $select->order('A.id','desc')->limit($offset, $limit)->field('A.*,B.name domain')->select()->toArray();
foreach($list as &$row){
$row['checktimestr'] = date('Y-m-d H:i:s', $row['checktime']);
}
return json(['total'=>$total, 'rows'=>$list]);
}
public function taskform()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
$action = input('param.action');
if(request()->isPost()){
if($action == 'add'){
$task = [
'did' => input('post.did/d'),
'rr' => input('post.rr', null, 'trim'),
'recordid' => input('post.recordid', null, 'trim'),
'type' => input('post.type/d'),
'main_value' => input('post.main_value', null, 'trim'),
'backup_value' => input('post.backup_value', null, 'trim'),
'checktype' => input('post.checktype/d'),
'checkurl' => input('post.checkurl', null, 'trim'),
'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null,
'frequency' => input('post.frequency/d'),
'cycle' => input('post.cycle/d'),
'timeout' => input('post.timeout/d'),
'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'),
'remark' => input('post.remark', null, 'trim'),
'recordinfo' => input('post.recordinfo', null, 'trim'),
];
if(empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])){
return json(['code'=>-1, 'msg'=>'必填项不能为空']);
}
if($task['checktype'] > 0 && $task['timeout'] > $task['frequency']){
return json(['code'=>-1, 'msg'=>'为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']);
}
if($task['type'] == 2 && $task['backup_value'] == $task['main_value']){
return json(['code'=>-1, 'msg'=>'主备地址不能相同']);
}
if(Db::name('dmtask')->where('recordid', $task['recordid'])->where('id', '<>', $id)->find()){
return json(['code'=>-1, 'msg'=>'当前容灾切换策略已存在']);
}
Db::name('dmtask')->where('id', $id)->update($task);
return json(['code'=>0, 'msg'=>'修改成功']);
}elseif($action == 'setactive'){
$id = input('post.id/d');
$active = input('post.active/d');
Db::name('dmtask')->where('id', $id)->update(['active'=>$active]);
return json(['code'=>0, 'msg'=>'设置成功']);
}elseif($action == 'del'){
$id = input('post.id/d');
Db::name('dmtask')->where('id', $id)->delete();
Db::name('dmlog')->where('taskid', $id)->delete();
return json(['code'=>0, 'msg'=>'删除成功']);
}else{
return json(['code'=>-1, 'msg'=>'参数错误']);
}
}
$task = null;
if($action == 'edit'){
$id = input('get.id/d');
$task = Db::name('dmtask')->where('id', $id)->find();
if(empty($task)) return $this->alert('error', '任务不存在');
}
$domains = [];
foreach(Db::name('domain')->select() as $row){
$domains[$row['id']] = $row['name'];
}
View::assign('domains', $domains);
View::assign('info', $task);
View::assign('action', $action);
View::assign('support_ping', function_exists('exec')?'1':'0');
return View::fetch('taskform');
}
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'] == 2){
$task['action_name'] = ['未知', '<font color="red">切换备用解析记录</font>', '<font color="green">恢复主解析记录</font>'];
}else{
$task['action_name'] = ['未知', '<font color="red">暂停解析</font>', '<font color="green">启用解析</font>'];
}
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 noticeset()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
if(request()->isPost()){
$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']);
}
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\lib\MsgNotice::send_mail($mail_name,'邮件发送测试。','这是一封测试邮件!<br/><br/>来自:'.request()->root(true));
if($result === true){
return json(['code'=>0, 'msg'=>'邮件发送成功!']);
}else{
return json(['code'=>-1, 'msg'=>'邮件发送失败!'.$result]);
}
}
public function clean()
{
if(!checkPermission(2)) return $this->alert('error', '无权限');
if(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';
}
}
<?php
namespace app\controller;
use app\BaseController;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
class Dmonitor extends BaseController
{
public function overview()
{
if (!checkPermission(2)) return $this->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') ? '<font color="green">已安装</font>' : '<font color="red">未安装</font>',
]);
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'] = ['未知', '<font color="red">开启解析</font>', '<font color="green">暂停解析</font>'];
} elseif ($task['type'] == 2) {
$task['action_name'] = ['未知', '<font color="red">切换备用解析记录</font>', '<font color="green">恢复主解析记录</font>'];
} else {
$task['action_name'] = ['未知', '<font color="red">暂停解析</font>', '<font color="green">启用解析</font>'];
}
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';
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
<?php
namespace app\controller;
use app\BaseController;
@@ -6,33 +7,52 @@ use Exception;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
use app\lib\DnsHelper;
class Index extends BaseController
{
public function index()
{
if(request()->user['type'] == 'domain'){
return redirect('/record/'.request()->user['id']);
if ($this->request->user['type'] == 'domain') {
return redirect('/record/' . $this->request->user['id']);
}
if(request()->isAjax()){
if(input('post.do') == 'stat'){
$stat = ['domains'=>0, 'users'=>0, 'records'=>0, 'types'=>count(DnsHelper::$dns_config)];
if(request()->user['level'] == 2){
if ($this->request->isAjax()) {
if (input('post.do') == 'stat') {
$stat = [];
if ($this->request->user['level'] == 2) {
$stat['domains'] = Db::name('domain')->count();
$stat['users'] = Db::name('user')->count();
$stat['records'] = Db::name('domain')->sum('recordcount');
}else{
$stat['domains'] = Db::name('domain')->where('name', 'in', request()->user['permission'])->count();
$stat['users'] = 1;
$stat['records'] = Db::name('domain')->where('name', 'in', request()->user['permission'])->sum('recordcount');
} else {
$stat['domains'] = Db::name('domain')->where('name', 'in', $this->request->user['permission'])->count();
}
$stat['tasks'] = Db::name('dmtask')->count();
$stat['certs'] = Db::name('cert_order')->count();
$stat['deploys'] = Db::name('cert_deploy')->count();
$run_time = config_get('run_time', null, true);
$run_state = $run_time ? (time() - strtotime($run_time) > 10 ? 0 : 1) : 0;
$stat['dmonitor_state'] = $run_state;
$stat['dmonitor_active'] = Db::name('dmtask')->where('active', 1)->count();
$stat['dmonitor_status_0'] = Db::name('dmtask')->where('status', 0)->count();
$stat['dmonitor_status_1'] = Db::name('dmtask')->where('status', 1)->count();
$stat['optimizeip_active'] = Db::name('optimizeip')->where('active', 1)->count();
$stat['optimizeip_status_1'] = Db::name('optimizeip')->where('status', 1)->count();
$stat['optimizeip_status_2'] = Db::name('optimizeip')->where('status', 2)->count();
$stat['certorder_status_3'] = Db::name('cert_order')->where('status', 3)->count();
$stat['certorder_status_5'] = Db::name('cert_order')->where('status', '<', 0)->count();
$stat['certorder_status_6'] = Db::name('cert_order')->where('expiretime', '<', date('Y-m-d H:i:s', time() + 86400 * 7))->where('expiretime', '>=', date('Y-m-d H:i:s'))->count();
$stat['certorder_status_7'] = Db::name('cert_order')->where('expiretime', '<', date('Y-m-d H:i:s'))->count();
$stat['certdeploy_status_0'] = Db::name('cert_deploy')->where('status', 0)->count();
$stat['certdeploy_status_1'] = Db::name('cert_deploy')->where('status', 1)->count();
$stat['certdeploy_status_2'] = Db::name('cert_deploy')->where('status', -1)->count();
return json($stat);
}
return json(['code'=>-3]);
return json(['code' => -3]);
}
if(config('app.dbversion') && config_get('version') != config('app.dbversion')){
if (config('app.dbversion') && config_get('version') != config('app.dbversion')) {
$this->db_update();
config_set('version', config('app.dbversion'));
Cache::clear();
@@ -41,7 +61,7 @@ class Index extends BaseController
$tmp = 'version()';
$mysqlVersion = Db::query("select version()")[0][$tmp];
$info = [
'framework_version' => app()::VERSION,
'framework_version' => app()->version(),
'php_version' => PHP_VERSION,
'mysql_version' => $mysqlVersion,
'software' => $_SERVER['SERVER_SOFTWARE'],
@@ -49,67 +69,116 @@ class Index extends BaseController
'date' => date("Y-m-d H:i:s"),
];
View::assign('info', $info);
View::assign('checkupdate', '//auth.cccyun.cc/app/dnsmgr.php?ver='.config('app.version'));
View::assign('checkupdate', '//auth.cccyun.cc/app/dnsmgr.php?ver=' . config('app.version'));
return view();
}
private function db_update(){
$sqls=file_get_contents(app()->getAppPath().'sql/update.sql');
private function db_update()
{
$sqls = file_get_contents(app()->getAppPath() . 'sql/update.sql');
$mysql_prefix = env('database.prefix', 'dnsmgr_');
$sqls=explode(';', $sqls);
$sqls = explode(';', $sqls);
foreach ($sqls as $value) {
$value=trim($value);
if(empty($value))continue;
$value = str_replace('dnsmgr_',$mysql_prefix,$value);
Db::execute($value);
$value = trim($value);
if (empty($value)) continue;
$value = str_replace('dnsmgr_', $mysql_prefix, $value);
try {
Db::execute($value);
} catch (Exception $e) {
}
}
}
public function changeskin(){
public function changeskin()
{
$skin = input('post.skin');
if(request()->user['level'] == 2){
if(cookie('admin_skin')){
if ($this->request->user['level'] == 2) {
if (cookie('admin_skin')) {
cookie('admin_skin', null);
}
config_set('admin_skin', $skin);
Cache::delete('configs');
}else{
Cache::delete('configs');
} else {
cookie('admin_skin', $skin);
}
return json(['code'=>0,'msg'=>'succ']);
return json(['code' => 0, 'msg' => 'succ']);
}
public function cleancache(){
if(!checkPermission(1)) return $this->alert('error', '无权限');
public function cleancache()
{
if (!checkPermission(1)) return $this->alert('error', '无权限');
Cache::clear();
return json(['code'=>0,'msg'=>'succ']);
clearDirectory(app()->getRuntimePath().'cache/');
clearDirectory(app()->getRuntimePath().'temp/');
return json(['code' => 0, 'msg' => 'succ']);
}
public function doc(){
if(!checkPermission(1)) return $this->alert('error', '无权限');
View::assign('siteurl', request()->root(true));
public function doc()
{
if (!checkPermission(1)) return $this->alert('error', '无权限');
View::assign('siteurl', $this->request->root(true));
return view();
}
public function setpwd(){
if(!checkPermission(1)) return $this->alert('error', '无权限');
if(request()->isPost()){
public function setpwd()
{
if (!checkPermission(1)) return $this->alert('error', '无权限');
if ($this->request->isPost()) {
$oldpwd = input('post.oldpwd');
$newpwd = input('post.newpwd');
$newpwd2 = input('post.newpwd2');
if(empty($oldpwd) || empty($newpwd) || empty($newpwd2)){
return json(['code'=>-1, 'msg'=>'密码不能为空']);
if (empty($oldpwd) || empty($newpwd) || empty($newpwd2)) {
return json(['code' => -1, 'msg' => '密码不能为空']);
}
if($newpwd != $newpwd2){
return json(['code'=>-1, 'msg'=>'两次输入的密码不一致']);
if ($newpwd != $newpwd2) {
return json(['code' => -1, 'msg' => '两次输入的密码不一致']);
}
if(!password_verify($oldpwd, request()->user['password'])){
return json(['code'=>-1, 'msg'=>'原密码错误']);
if (!password_verify($oldpwd, $this->request->user['password'])) {
return json(['code' => -1, 'msg' => '原密码错误']);
}
Db::name('user')->where('id', request()->user['id'])->update(['password'=>password_hash($newpwd, PASSWORD_DEFAULT)]);
return json(['code'=>0, 'msg'=>'succ']);
Db::name('user')->where('id', $this->request->user['id'])->update(['password' => password_hash($newpwd, PASSWORD_DEFAULT)]);
return json(['code' => 0, 'msg' => 'succ']);
}
View::assign('user', $this->request->user);
return view();
}
public function totp()
{
if (!checkPermission(1)) return $this->alert('error', '无权限');
$action = input('param.action');
if ($action == 'generate') {
try {
$totp = \app\lib\TOTP::create();
$totp->setLabel($this->request->user['username']);
$totp->setIssuer('DNS Manager');
return json(['code' => 0, 'data' => ['secret' => $totp->getSecret(), 'qrcode' => $totp->getProvisioningUri()]]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
} elseif ($action == 'bind') {
$secret = input('post.secret');
$code = input('post.code');
if (empty($secret)) return json(['code' => -1, 'msg' => '密钥不能为空']);
if (empty($code)) return json(['code' => -1, 'msg' => '请输入动态口令']);
try {
$totp = \app\lib\TOTP::create($secret);
if (!$totp->verify($code)) {
return json(['code' => -1, 'msg' => '动态口令错误']);
}
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
Db::name('user')->where('id', $this->request->user['id'])->update(['totp_open' => 1, 'totp_secret' => $secret]);
return json(['code' => 0, 'msg' => 'succ']);
} elseif ($action == 'close') {
Db::name('user')->where('id', $this->request->user['id'])->update(['totp_open' => 0, 'totp_secret' => null]);
return json(['code' => 0, 'msg' => 'succ']);
}
}
public function test()
{
}
}

View File

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

View File

@@ -0,0 +1,183 @@
<?php
namespace app\controller;
use app\BaseController;
use Exception;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
use app\service\OptimizeService;
class Optimizeip extends BaseController
{
public function opipset()
{
if (!checkPermission(2)) return $this->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';
}
}

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

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

149
app/controller/System.php Normal file
View File

@@ -0,0 +1,149 @@
<?php
namespace app\controller;
use app\BaseController;
use Exception;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
use app\service\OptimizeService;
use app\service\CertTaskService;
use app\service\ExpireNoticeService;
use app\service\ScheduleService;
class System extends BaseController
{
public function set()
{
if (!checkPermission(2)) return $this->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, '邮件发送测试。', '这是一封测试邮件!<br/><br/>来自:' . $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 = "<strong>消息发送测试</strong>\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!';
}
}

View File

@@ -1,164 +1,188 @@
<?php
namespace app\controller;
use app\BaseController;
use think\facade\Db;
use think\facade\View;
use think\facade\Request;
class User extends BaseController
{
public function user(){
if(!checkPermission(2)) return $this->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 == 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 == 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 == 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(request()->user['type'] == 'domain'){
$select->where('domain', request()->user['name']);
}elseif(request()->user['level'] == 1){
$select->where('uid', request()->user['id']);
}elseif(!empty($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]);
}
}
<?php
namespace app\controller;
use app\BaseController;
use think\facade\Db;
use think\facade\View;
use think\facade\Request;
class User extends BaseController
{
public function user()
{
if (!checkPermission(2)) return $this->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]);
}
}

1944
app/data/domain_root.txt Normal file

File diff suppressed because it is too large Load Diff

433
app/lib/CertHelper.php Normal file
View File

@@ -0,0 +1,433 @@
<?php
namespace app\lib;
use think\facade\Db;
class CertHelper
{
public static $cert_config = [
'letsencrypt' => [
'name' => 'Let\'s Encrypt',
'class' => 1,
'icon' => 'letsencrypt.ico',
'wildcard' => true,
'max_domains' => 100,
'cname' => true,
'note' => null,
'inputs' => [
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => '用于注册Let\'s Encrypt账号',
'required' => true,
],
'mode' => [
'name' => '环境选择',
'type' => 'radio',
'options' => [
'live' => '正式环境',
'staging' => '测试环境',
],
'value' => 'live'
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
]
],
'zerossl' => [
'name' => 'ZeroSSL',
'class' => 1,
'icon' => 'zerossl.ico',
'wildcard' => true,
'max_domains' => 100,
'cname' => true,
'note' => '<a href="https://app.zerossl.com/developer" target="_blank" rel="noreferrer">ZeroSSL密钥手动获取</a>',
'inputs' => [
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => 'EAB申请邮箱',
'required' => true,
],
'eabMode' => [
'name' => 'EAB获取方式',
'type' => 'radio',
'options' => [
'auto' => '自动获取',
'manual' => '手动输入',
],
'value' => 'manual'
],
'kid' => [
'name' => 'EAB KID',
'type' => 'input',
'placeholder' => '',
'required' => true,
'show' => 'eabMode==\'manual\'',
],
'key' => [
'name' => 'EAB HMAC Key',
'type' => 'input',
'placeholder' => '',
'required' => true,
'show' => 'eabMode==\'manual\'',
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
]
],
'google' => [
'name' => 'Google SSL',
'class' => 1,
'icon' => 'google.ico',
'wildcard' => true,
'max_domains' => 100,
'cname' => true,
'note' => 'EAB支持通过第三方接口<a href="https://panel.haozi.net" target="_blank" rel="noreferrer">(耗子面板提供)</a>自动获取(不支持测试环境)或手动输入,<a href="https://cloud.google.com/certificate-manager/docs/public-ca-tutorial" target="_blank" rel="noreferrer">查看Google SSL账户手动配置说明</a>',
'inputs' => [
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => 'EAB申请邮箱',
'required' => true,
],
'eabMode' => [
'name' => 'EAB获取方式',
'type' => 'radio',
'options' => [
'auto' => '自动获取',
'manual' => '手动输入',
],
'value' => 'manual'
],
'kid' => [
'name' => 'keyId',
'type' => 'input',
'placeholder' => '',
'required' => true,
'show' => 'eabMode==\'manual\'',
],
'key' => [
'name' => 'b64MacKey',
'type' => 'input',
'placeholder' => '',
'required' => true,
'show' => 'eabMode==\'manual\'',
],
'mode' => [
'name' => '环境选择',
'type' => 'radio',
'options' => [
'live' => '正式环境',
'staging' => '测试环境',
],
'value' => 'live'
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
'2' => '是(反向代理)'
],
'value' => '0'
],
'proxy_url' => [
'name' => '反向代理地址',
'type' => 'input',
'placeholder' => 'https://gts.rat.dev',
'required' => true,
'show' => 'proxy==2',
'note' => '反向代理配置参考:
<pre>resolver 8.8.8.8 ipv6=off valid=300s;
resolver_timeout 10s;
location / {
set $empty "";
proxy_pass https://dv.acme-v02.api.pki.goog$empty;
proxy_set_header Accept-Encoding "";
proxy_ssl_session_reuse off;
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_http_version 1.1;
sub_filter_once off;
sub_filter_types *;
sub_filter \'dv.acme-v02.api.pki.goog\' \'gts.rat.dev\'; # 替换自己的域名
}</pre>',
],
]
],
'tencent' => [
'name' => '腾讯云免费SSL',
'class' => 2,
'icon' => 'tencent.png',
'wildcard' => false,
'max_domains' => 1,
'cname' => false,
'note' => '一个账号有50张免费证书额度证书到期或吊销可释放额度。<a href="https://cloud.tencent.com/document/product/400/89868" target="_blank" rel="noreferrer">腾讯云免费SSL简介与额度说明</a>',
'inputs' => [
'SecretId' => [
'name' => 'SecretId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'SecretKey' => [
'name' => 'SecretKey',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => '申请证书时填写的邮箱',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
]
],
'aliyun' => [
'name' => '阿里云免费SSL',
'class' => 2,
'icon' => 'aliyun.png',
'wildcard' => false,
'max_domains' => 1,
'cname' => false,
'note' => '每个自然年有20张免费证书额度证书到期或吊销不释放额度。需要先进入阿里云控制台-<a href="https://yundun.console.aliyun.com/?p=cas#/certExtend/free/cn-hangzhou" target="_blank" rel="noreferrer">数字证书管理服务</a>,购买个人测试证书资源包。',
'inputs' => [
'AccessKeyId' => [
'name' => 'AccessKeyId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'AccessKeySecret' => [
'name' => 'AccessKeySecret',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'username' => [
'name' => '姓名',
'type' => 'input',
'placeholder' => '申请联系人的姓名',
'required' => true,
],
'phone' => [
'name' => '手机号码',
'type' => 'input',
'placeholder' => '申请联系人的手机号码',
'required' => true,
],
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => '申请联系人的邮箱地址',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
]
],
'ucloud' => [
'name' => 'UCloud免费SSL',
'class' => 2,
'icon' => 'ucloud.ico',
'wildcard' => false,
'max_domains' => 1,
'cname' => false,
'note' => '一个账号有40张免费证书额度证书到期或吊销可释放额度。',
'inputs' => [
'PublicKey' => [
'name' => '公钥',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'PrivateKey' => [
'name' => '私钥',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'username' => [
'name' => '姓名',
'type' => 'input',
'placeholder' => '申请联系人的姓名',
'required' => true,
],
'phone' => [
'name' => '手机号码',
'type' => 'input',
'placeholder' => '申请联系人的手机号码',
'required' => true,
],
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => '申请联系人的邮箱地址',
'required' => true,
],
]
],
'customacme' => [
'name' => '自定义ACME',
'class' => 1,
'icon' => 'ssl.ico',
'wildcard' => true,
'max_domains' => 100,
'cname' => true,
'note' => null,
'inputs' => [
'directory' => [
'name' => 'ACME地址',
'type' => 'input',
'placeholder' => 'ACME Directory 地址',
'required' => true,
],
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => '证书申请邮箱',
'required' => true,
],
'kid' => [
'name' => 'EAB KID',
'type' => 'input',
'placeholder' => '留空则不使用EAB认证',
],
'key' => [
'name' => 'EAB HMAC Key',
'type' => 'input',
'placeholder' => '留空则不使用EAB认证',
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
]
],
];
public static $class_config = [
1 => '基于ACME的SSL证书',
2 => '云服务商的SSL证书',
];
public static function getList()
{
return self::$cert_config;
}
private static function getConfig($aid)
{
$account = Db::name('cert_account')->where('id', $aid)->find();
if (!$account) return false;
return $account;
}
public static function getInputs($type, $config = null)
{
$config = $config ? json_decode($config, true) : [];
$inputs = self::$cert_config[$type]['inputs'];
foreach ($inputs as &$input) {
if (isset($config[$input['name']])) {
$input['value'] = $config[$input['name']];
}
}
return $inputs;
}
/**
* @return CertInterface|bool
*/
public static function getModel($aid)
{
$account = self::getConfig($aid);
if (!$account) return false;
$type = $account['type'];
$class = "\\app\\lib\\cert\\{$type}";
if (class_exists($class)) {
$config = json_decode($account['config'], true);
$ext = $account['ext'] ? json_decode($account['ext'], true) : null;
$model = new $class($config, $ext);
return $model;
}
return false;
}
/**
* @return CertInterface|bool
*/
public static function getModel2($type, $config, $ext = null)
{
$class = "\\app\\lib\\cert\\{$type}";
if (class_exists($class)) {
$model = new $class($config, $ext);
return $model;
}
return false;
}
/**
* 确保ECC私钥使用EC专用格式标识
* 某些程序需要EC标识才能正确识别ECC私钥
*/
public static function ensureECPrivateKeyFormat($private_key)
{
if (strpos($private_key, '-----BEGIN EC PRIVATE KEY-----') !== false) {
return $private_key;
}
if (strpos($private_key, '-----BEGIN PRIVATE KEY-----') !== false) {
$private_key = preg_replace('/^-----BEGIN PRIVATE KEY-----$/m', '-----BEGIN EC PRIVATE KEY-----', $private_key);
$private_key = preg_replace('/^-----END PRIVATE KEY-----$/m', '-----END EC PRIVATE KEY-----', $private_key);
}
return $private_key;
}
public static function getPfx($fullchain, $privatekey, $pwd = '123456')
{
openssl_pkcs12_export($fullchain, $pfx, $privatekey, $pwd);
return $pfx;
}
}

24
app/lib/CertInterface.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
namespace app\lib;
interface CertInterface
{
function register();
function buyCert($domainList, &$order);
function createOrder($domainList, &$order, $keytype, $keysize);
function authOrder($domainList, $order);
function getAuthStatus($domainList, $order);
function finalizeOrder($domainList, $order, $keytype, $keysize);
function revoke($order, $pem);
function cancel($order);
function setLogger($func);
}

View File

@@ -1,90 +0,0 @@
<?php
namespace app\lib;
class CheckUtils
{
public static function curl($url, $timeout, $ip = null)
{
$status = true;
$errmsg = null;
$urlarr = parse_url($url);
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 = isset($urlarr['port']) ? $urlarr['port'] : ($urlarr['scheme'] == 'https' ? 443 : 80);
$resolve = $urlarr['host'] . ':' . $port . ':' . $ip;
}
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$httpheader[] = "Accept: */*";
$httpheader[] = "Accept-Language: zh-CN,zh;q=0.8";
$httpheader[] = "Connection: close";
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
if(!empty($resolve)){
curl_setopt($ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false);
curl_setopt($ch, CURLOPT_RESOLVE, [$resolve]);
}
curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$status = false;
$errmsg = curl_error($ch);
}
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($status && ($httpcode < 200 || $httpcode >= 400)){
$status = false;
$errmsg = 'http_code='.$httpcode;
}
$usetime = round(curl_getinfo($ch, CURLINFO_TOTAL_TIME) * 1000);
curl_close($ch);
return ['status'=>$status, 'errmsg'=>$errmsg, 'usetime'=>$usetime];
}
public static function tcp($target, $port, $timeout){
if(!filter_var($target,FILTER_VALIDATE_IP) && checkDomain($target)){
$target = gethostbyname($target);
if(!$target)return ['status'=>false, 'error'=>'DNS resolve failed', 'usetime'=>0];
}
$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){
if(!function_exists('exec'))return ['status'=>false, 'error'=>'exec函数不可用', 'usetime'=>0];
if(!filter_var($target,FILTER_VALIDATE_IP) && checkDomain($target)){
$target = gethostbyname($target);
if(!$target)return ['status'=>false, 'error'=>'DNS resolve failed', 'usetime'=>0];
}
if(!filter_var($target,FILTER_VALIDATE_IP)){
return ['status'=>false, 'error'=>'Invalid IP address', 'usetime'=>0];
}
$timeout = 1;
exec('ping -c 1 -w '.$timeout.' '.$target.'', $output, $return_var);
$usetime = !empty($output[1]) ? round(getSubstr($output[1], 'time=', ' ms')) : 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];
}
}

2562
app/lib/DeployHelper.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
<?php
namespace app\lib;
interface DeployInterface
{
function check();
function deploy($fullchain, $privatekey, $config, &$info);
function setLogger($func);
}

View File

@@ -1,115 +1,241 @@
<?php
namespace app\lib;
use think\facade\Db;
class DnsHelper
{
public static $dns_config = [
'aliyun' => [
'name' => '阿里云',
'config' => [
'ak' => 'AccessKeyId',
'sk' => 'AccessKeySecret'
],
'remark' => 1, //是否支持备注1单独设置备注2和记录一起设置
'status' => true, //是否支持启用暂停
'redirect' => true, //是否支持域名转发
'log' => true, //是否支持查看日志
],
'dnspod' => [
'name' => '腾讯云',
'config' => [
'ak' => 'SecretId',
'sk' => 'SecretKey'
],
'remark' => 1,
'status' => true,
'redirect' => true,
'log' => true,
],
'huawei' => [
'name' => '华为云',
'config' => [
'ak' => 'AccessKeyId',
'sk' => 'SecretAccessKey'
],
'remark' => 2,
'status' => true,
'redirect' => false,
'log' => false,
],
'west' => [
'name' => '西部数码',
'config' => [
'ak' => '用户名',
'sk' => 'API密码'
],
'remark' => 0,
'status' => true,
'redirect' => false,
'log' => false,
],
'dnsla' => [
'name' => 'DNSLA',
'config' => [
'ak' => 'APIID',
'sk' => 'API密钥'
],
'remark' => 0,
'status' => true,
'redirect' => true,
'log' => false,
],
'cloudflare' => [
'name' => 'Cloudflare',
'config' => [
'ak' => '邮箱地址',
'sk' => 'API密钥'
],
'remark' => 2,
'status' => false,
'redirect' => false,
'log' => false,
],
];
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;
}
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;
}
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;
}
}
<?php
namespace app\lib;
use think\facade\Db;
class DnsHelper
{
public static $dns_config = [
'aliyun' => [
'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,
],
'spaceship' => [
'name' => 'Spaceship',
'config' => [
'ak' => 'AccessKey',
'sk' => 'SecretKey',
],
'remark' => 0,
'status' => false,
'redirect' => true,
'log' => false,
'weight' => false,
'page' => false,
'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'],
'spaceship' => ['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;
}
}

View File

@@ -1,35 +1,36 @@
<?php
namespace app\lib;
interface DnsInterface
{
function getError();
function check();
function getDomainList($KeyWord=null, $PageNumber=1, $PageSize=20);
function getDomainRecords($PageNumber=1, $PageSize=20, $KeyWord = null, $SubDomain = null, $Type = null, $Line = null, $Status = null);
function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null);
function getDomainRecordInfo($RecordId);
function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Remark = null);
function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Remark = null);
function updateDomainRecordRemark($RecordId, $Remark);
function deleteDomainRecord($RecordId);
function setDomainRecordStatus($RecordId, $Status);
function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null);
function getRecordLine();
function getMinTTL();
}
<?php
namespace app\lib;
interface DnsInterface
{
function getError();
function check();
function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20);
function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null);
function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null);
function getDomainRecordInfo($RecordId);
function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null);
function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null);
function updateDomainRecordRemark($RecordId, $Remark);
function deleteDomainRecord($RecordId);
function setDomainRecordStatus($RecordId, $Status);
function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null);
function getRecordLine();
function getMinTTL();
function addDomain($Domain);
}

View File

@@ -1,105 +0,0 @@
<?php
namespace app\lib;
class MsgNotice
{
private static $sitename = '聚合DNS管理系统';
public static function send($action, $task, $result)
{
if($action == 1){
$mail_title = 'DNS容灾切换-发生告警通知';
$mail_content = '尊敬的系统管理员,您好:<br/>您的域名 <b>'.$task['domain'].'</b> 的 <b>'.$task['main_value'].'</b> 记录发生了异常';
if($task['type'] == 2){
$mail_content .= ',已自动切换为备用解析记录 '.$task['backup_value'].' ';
}elseif($task['type'] == 1){
$mail_content .= ',已自动暂停解析';
}else{
$mail_content .= ',请及时处理';
}
if(!empty($result['errmsg'])){
$mail_content .= '。<br/>异常信息:'.$result['errmsg'];
}
}else{
$mail_title = 'DNS容灾切换-恢复正常通知';
$mail_content = '尊敬的系统管理员,您好:<br/>您的域名 <b>'.$task['domain'].'</b> 的 <b>'.$task['main_value'].'</b> 记录已恢复正常';
if($task['type'] == 2){
$mail_content .= ',已自动切换回当前解析记录';
}elseif($task['type'] == 1){
$mail_content .= ',已自动开启解析';
}
$lasttime = convert_second(time() - $task['switchtime']);
$mail_content .= '。<br/>异常持续时间:'.$lasttime;
}
if(!empty($task['remark'])) $mail_title .= '('.$task['remark'].')';
if(!empty($task['remark'])) $mail_content .= '<br/>备注:'.$task['remark'];
$mail_content .= '<br/>'.self::$sitename.'<br/>'.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){
$mail_content = str_replace(['<br/>', '<b>', '</b>'], ["\n\n", '**', '**'], $mail_content);
self::send_wechat_tplmsg($mail_title, $mail_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 \app\lib\mail\PHPMailer\PHPMailer(true);
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,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'];
}
}
}

View File

@@ -10,11 +10,11 @@ class NewDbManager extends \think\Db
/**
* 创建数据库连接实例
* @access protected
* @param string|null $name 连接标识
* @param string|array|null $name 连接标识
* @param bool $force 强制重新连接
* @return ConnectionInterface
*/
protected function instance(string $name = null, bool $force = false): ConnectionInterface
protected function instance(string|array $name = null, bool $force = false): ConnectionInterface
{
if (empty($name)) {
$name = $this->getConfig('default', 'mysql');

228
app/lib/TOTP.php Normal file
View File

@@ -0,0 +1,228 @@
<?php
namespace app\lib;
class TOTP
{
private static $BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
private $period = 30;
private $digest = 'sha1';
private $digits = 6;
private $epoch = 0;
private $secret;
private $issuer;
private $label;
public function __construct(?string $secret)
{
if ($secret == null) {
$secret = $this->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;
}
}

View File

@@ -1,111 +0,0 @@
<?php
namespace app\lib;
use app\lib\NewDb;
use app\lib\CheckUtils;
use app\lib\DnsHelper;
use app\lib\MsgNotice;
class TaskRunner
{
private $conn;
private function db()
{
if(!$this->conn){
$this->conn = NewDb::connect();
}
return $this->conn;
}
private function closeDb()
{
if($this->conn){
$this->conn->close();
}
}
public function execute($row)
{
if($row['checktype'] == 2){
$result = CheckUtils::curl($row['checkurl'], $row['timeout'], $row['main_value']);
}else if($row['checktype'] == 1){
$result = CheckUtils::tcp($row['main_value'], $row['tcpport'], $row['timeout']);
}else{
$result = CheckUtils::ping($row['main_value']);
}
$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);
$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){
$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);
$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){
$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' => $result['status'] ? null : $result['errmsg'],
'date' => date('Y-m-d H:i:s')
]);
$this->closeDb();
MsgNotice::send($action, $row, $result);
}
}

686
app/lib/acme/ACMECert.php Normal file
View File

@@ -0,0 +1,686 @@
<?php
namespace app\lib\acme;
use Exception;
use stdClass;
use app\lib\CertHelper;
/**
* ACMECert
* https://github.com/skoerfgen/ACMECert
*/
class ACMECert extends ACMEv2
{
private $alternate_chains = array();
public function register($termsOfServiceAgreed = false, $contacts = array())
{
return $this->_register($termsOfServiceAgreed, $contacts);
}
public function registerEAB($termsOfServiceAgreed, $eab_kid, $eab_hmac, $contacts = array())
{
if (!$this->resources) $this->readDirectory();
$protected = array(
'alg' => 'HS256',
'kid' => $eab_kid,
'url' => $this->unproxiedURL($this->resources['newAccount'])
);
$payload = $this->jwk_header['jwk'];
$protected64 = $this->base64url(json_encode($protected, JSON_UNESCAPED_SLASHES));
$payload64 = $this->base64url(json_encode($payload, JSON_UNESCAPED_SLASHES));
$signature = hash_hmac('sha256', $protected64 . '.' . $payload64, $this->base64url_decode($eab_hmac), true);
return $this->_register($termsOfServiceAgreed, $contacts, array(
'externalAccountBinding' => array(
'protected' => $protected64,
'payload' => $payload64,
'signature' => $this->base64url($signature)
)
));
}
private function _register($termsOfServiceAgreed = false, $contacts = array(), $extra = array())
{
$this->log('Registering account');
$ret = $this->request('newAccount', array(
'termsOfServiceAgreed' => (bool)$termsOfServiceAgreed,
'contact' => $this->make_contacts_array($contacts)
) + $extra);
$this->log($ret['code'] == 201 ? 'Account registered' : 'Account already registered');
return $this->kid_header['kid'];
}
public function update($contacts = array())
{
$this->log('Updating account');
$ret = $this->request($this->getAccountID(), array(
'contact' => $this->make_contacts_array($contacts)
));
$this->log('Account updated');
return $ret['body'];
}
public function getAccount()
{
$ret = parent::getAccount();
return $this->kid_header['kid'];
}
public function setAccount($kid)
{
$this->kid_header['kid'] = $kid;
}
public function deactivateAccount()
{
$this->log('Deactivating account');
$ret = $this->deactivate($this->getAccountID());
$this->log('Account deactivated');
return $ret;
}
public function deactivate($url)
{
$this->log('Deactivating resource: ' . $url);
$ret = $this->request($url, array('status' => 'deactivated'));
$this->log('Resource deactivated');
return $ret['body'];
}
public function getTermsURL()
{
if (!$this->resources) $this->readDirectory();
if (!isset($this->resources['meta']['termsOfService'])) {
throw new Exception('Failed to get Terms Of Service URL');
}
return $this->resources['meta']['termsOfService'];
}
public function getCAAIdentities()
{
if (!$this->resources) $this->readDirectory();
if (!isset($this->resources['meta']['caaIdentities'])) {
throw new Exception('Failed to get CAA Identities');
}
return $this->resources['meta']['caaIdentities'];
}
public function keyChange($new_account_key_pem)
{ // account key roll-over
$this->loadAccountKey($new_account_key_pem);
$account = $this->getAccountID();
$this->resources = $this->resources;
$this->log('Account Key Roll-Over');
$ret = $this->request(
'keyChange',
$this->jws_encapsulate('keyChange', array(
'account' => $account,
'oldKey' => $this->jwk_header['jwk']
), true)
);
$this->log('Account Key Roll-Over successful');
$this->loadAccountKey($new_account_key_pem);
return $ret['body'];
}
public function revoke($pem)
{
if (false === ($res = openssl_x509_read($pem))) {
throw new Exception('Could not load certificate: ' . $pem . ' (' . $this->get_openssl_error() . ')');
}
if (false === (openssl_x509_export($res, $certificate))) {
throw new Exception('Could not export certificate: ' . $pem . ' (' . $this->get_openssl_error() . ')');
}
$this->log('Revoking certificate');
$this->request('revokeCert', array(
'certificate' => $this->base64url($this->pem2der($certificate))
));
$this->log('Certificate revoked');
}
public function createOrder($domain_config, $settings = array())
{
$settings = $this->parseSettings($settings);
$domain_config = array_change_key_case($domain_config, CASE_LOWER);
$domains = array_keys($domain_config);
$authz_deactivated = false;
// === Order ===
$this->log('Creating Order');
$ret = $this->request('newOrder', $this->makeOrder($domains, $settings));
$order = $ret['body'];
$order_location = $ret['headers']['location'];
$this->log('Order created: ' . $order_location);
$order['location'] = $order_location;
// === Authorization ===
if ($order['status'] === 'ready' && $settings['authz_reuse']) {
$this->log('All authorizations already valid, skipping validation altogether');
} else {
$auth_count = count($order['authorizations']);
$challenges = array();
foreach ($order['authorizations'] as $idx => $auth_url) {
$this->log('Fetching authorization ' . ($idx + 1) . ' of ' . $auth_count);
$ret = $this->request($auth_url, '');
$authorization = $ret['body'];
// wildcard authorization identifiers have no leading *.
$domain = ( // get domain and add leading *. if wildcard is used
isset($authorization['wildcard']) &&
$authorization['wildcard'] ?
'*.' : ''
) . $authorization['identifier']['value'];
if ($authorization['status'] === 'valid') {
if ($settings['authz_reuse']) {
$this->log('Authorization of ' . $domain . ' already valid, skipping validation');
} else {
$this->log('Authorization of ' . $domain . ' already valid, deactivating authorization');
$this->deactivate($auth_url);
$authz_deactivated = true;
}
continue;
}
if(!isset($domain_config[$domain])) {
$this->log('Domain ' . $domain . ' not found in domain_config');
continue;
}
$config = $domain_config[$domain];
$type = $config['challenge'];
$challenge = $this->parse_challenges($authorization, $type, $challenge_url);
$opts = array(
'domain' => $domain,
'type' => $type,
'auth_url' => $auth_url,
'challenge_url' => $challenge_url
);
list($opts['key'], $opts['value']) = $challenge;
$challenges[] = $opts;
}
if ($authz_deactivated) {
$this->log('Restarting Order after deactivating already valid authorizations');
$settings['authz_reuse'] = true;
return $this->createOrder($domain_config, $settings);
}
$order['challenges'] = $challenges;
}
return $order;
}
public function authOrder($order)
{
if ($order['status'] != 'pending' && $order['status'] != 'ready' && empty($order['challenges'])) {
throw new Exception('No challenges available');
}
// === Challenge ===
if (!empty($order['challenges'])){
foreach ($order['challenges'] as $opts) {
$this->log('Notifying server for validation of ' . $opts['domain']);
$this->request($opts['challenge_url'], new stdClass);
$this->log('Waiting for server challenge validation');
sleep(1);
if (!$this->poll('pending', $opts['auth_url'], $body)) {
$this->log('Validation failed: ' . $opts['domain']);
$error = $body['challenges'][0]['error'];
throw $this->create_ACME_Exception(
$error['type'],
'Challenge validation failed: ' . $error['detail']
);
} else {
$this->log('Validation successful: ' . $opts['domain']);
}
}
}
}
public function finalizeOrder($domains, $order, $pem)
{
// autodetect if Private Key or CSR is used
if ($key = openssl_pkey_get_private($pem)) { // Private Key detected
if (PHP_MAJOR_VERSION < 8) openssl_free_key($key);
$this->log('Generating CSR');
$csr = $this->generateCSR($pem, $domains);
} elseif (openssl_csr_get_subject($pem)) { // CSR detected
$this->log('Using provided CSR');
if (0 === strpos($pem, 'file://')) {
$csr = file_get_contents(substr($pem, 7));
if (false === $csr) {
throw new Exception('Failed to read CSR from ' . $pem . ' (' . $this->get_openssl_error() . ')');
}
} else {
$csr = $pem;
}
} else {
throw new Exception('Could not load Private Key or CSR (' . $this->get_openssl_error() . '): ' . $pem);
}
$this->log('Finalizing Order');
$ret = $this->request($order['finalize'], array(
'csr' => $this->base64url($this->pem2der($csr))
));
$ret = $ret['body'];
if (isset($ret['certificate'])) {
return $this->request_certificate($ret);
}
if ($this->poll('processing', $order['location'], $ret)) {
return $this->request_certificate($ret);
}
throw new Exception('Order failed');
}
public function finalizeOrders($domains, $order, $pem)
{
$default_chain = $this->finalizeOrder($domains, $order, $pem);
$out = array();
$out[$this->getTopIssuerCN($default_chain)] = $default_chain;
foreach ($this->alternate_chains as $link) {
$chain = $this->request_certificate(array('certificate' => $link), true);
$out[$this->getTopIssuerCN($chain)] = $chain;
}
$this->log('Received ' . count($out) . ' chain(s): ' . implode(', ', array_keys($out)));
return $out;
}
public function generateCSR($domain_key_pem, $domains)
{
if (false === ($domain_key = openssl_pkey_get_private($domain_key_pem))) {
throw new Exception('Could not load domain key: ' . $domain_key_pem . ' (' . $this->get_openssl_error() . ')');
}
$fn = $this->tmp_ssl_cnf($domains);
$cn = reset($domains);
$dn = array();
if (strlen($cn) <= 64) {
$dn['commonName'] = $cn;
}
$csr = openssl_csr_new($dn, $domain_key, array(
'config' => $fn,
'req_extensions' => 'SAN',
'digest_alg' => 'sha512'
));
unlink($fn);
if (PHP_MAJOR_VERSION < 8) openssl_free_key($domain_key);
if (false === $csr) {
throw new Exception('Could not generate CSR ! (' . $this->get_openssl_error() . ')');
}
if (false === openssl_csr_export($csr, $out)) {
throw new Exception('Could not export CSR ! (' . $this->get_openssl_error() . ')');
}
return $out;
}
private function generateKey($opts)
{
$fn = $this->tmp_ssl_cnf();
$config = array('config' => $fn) + $opts;
if (false === ($key = openssl_pkey_new($config))) {
throw new Exception('Could not generate new private key ! (' . $this->get_openssl_error() . ')');
}
if (false === openssl_pkey_export($key, $pem, null, $config)) {
throw new Exception('Could not export private key ! (' . $this->get_openssl_error() . ')');
}
unlink($fn);
if (PHP_MAJOR_VERSION < 8) openssl_free_key($key);
return $pem;
}
public function generateRSAKey($bits = 2048)
{
return $this->generateKey(array(
'private_key_bits' => (int)$bits,
'private_key_type' => OPENSSL_KEYTYPE_RSA
));
}
public function generateECKey($curve_name = '384')
{
if (version_compare(PHP_VERSION, '7.1.0') < 0) throw new Exception('PHP >= 7.1.0 required for EC keys !');
$map = array('256' => 'prime256v1', '384' => 'secp384r1', '521' => 'secp521r1');
if (isset($map[$curve_name])) $curve_name = $map[$curve_name];
$pem = $this->generateKey(array(
'curve_name' => $curve_name,
'private_key_type' => OPENSSL_KEYTYPE_EC
));
return CertHelper::ensureECPrivateKeyFormat($pem);
}
public function parseCertificate($cert_pem)
{
if (false === ($ret = openssl_x509_read($cert_pem))) {
throw new Exception('Could not load certificate: ' . $cert_pem . ' (' . $this->get_openssl_error() . ')');
}
if (!is_array($ret = openssl_x509_parse($ret, true))) {
throw new Exception('Could not parse certificate (' . $this->get_openssl_error() . ')');
}
return $ret;
}
public function getSAN($pem)
{
$ret = $this->parseCertificate($pem);
if (!isset($ret['extensions']['subjectAltName'])) {
throw new Exception('No Subject Alternative Name (SAN) found in certificate');
}
$out = array();
foreach (explode(',', $ret['extensions']['subjectAltName']) as $line) {
list($type, $name) = array_map('trim', explode(':', $line));
if ($type === 'DNS') {
$out[] = $name;
}
}
return $out;
}
public function getRemainingDays($cert_pem)
{
$ret = $this->parseCertificate($cert_pem);
return ($ret['validTo_time_t'] - time()) / 86400;
}
public function getRemainingPercent($cert_pem)
{
$ret = $this->parseCertificate($cert_pem);
$total = $ret['validTo_time_t'] - $ret['validFrom_time_t'];
$used = time() - $ret['validFrom_time_t'];
return (1 - max(0, min(1, $used / $total))) * 100;
}
public function generateALPNCertificate($domain_key_pem, $domain, $token)
{
$domains = array($domain);
$csr = $this->generateCSR($domain_key_pem, $domains);
$fn = $this->tmp_ssl_cnf($domains, '1.3.6.1.5.5.7.1.31=critical,DER:0420' . $token . "\n");
$config = array(
'config' => $fn,
'x509_extensions' => 'SAN',
'digest_alg' => 'sha512'
);
$cert = openssl_csr_sign($csr, null, $domain_key_pem, 1, $config);
unlink($fn);
if (false === $cert) {
throw new Exception('Could not generate self signed certificate ! (' . $this->get_openssl_error() . ')');
}
if (false === openssl_x509_export($cert, $out)) {
throw new Exception('Could not export self signed certificate ! (' . $this->get_openssl_error() . ')');
}
return $out;
}
public function getARI($pem, &$ari_cert_id = null)
{
$ari_cert_id = null;
$id = $this->getARICertID($pem);
if (!$this->resources) $this->readDirectory();
if (!isset($this->resources['renewalInfo'])) throw new Exception('ARI not supported');
$ret = $this->http_request($this->resources['renewalInfo'] . '/' . $id);
if (!is_array($ret['body']['suggestedWindow'])) throw new Exception('ARI suggestedWindow not present');
$sw = &$ret['body']['suggestedWindow'];
if (!isset($sw['start'])) throw new Exception('ARI suggestedWindow start not present');
if (!isset($sw['end'])) throw new Exception('ARI suggestedWindow end not present');
$sw = array_map(array($this, 'parseDate'), $sw);
$ari_cert_id = $id;
return $ret['body'];
}
private function getARICertID($pem)
{
if (version_compare(PHP_VERSION, '7.1.2', '<')) {
throw new Exception('PHP Version >= 7.1.2 required for ARI'); // serialNumberHex - https://github.com/php/php-src/pull/1755
}
$ret = $this->parseCertificate($pem);
if (!isset($ret['extensions']['authorityKeyIdentifier'])) {
throw new Exception('authorityKeyIdentifier missing');
}
$aki = hex2bin(str_replace(':', '', substr(trim($ret['extensions']['authorityKeyIdentifier']), 6)));
if (!$aki) throw new Exception('Failed to parse authorityKeyIdentifier');
if (!isset($ret['serialNumberHex'])) {
throw new Exception('serialNumberHex missing');
}
$ser = hex2bin(trim($ret['serialNumberHex']));
if (!$ser) throw new Exception('Failed to parse serialNumberHex');
return $this->base64url($aki) . '.' . $this->base64url($ser);
}
private function parseDate($str)
{
$ret = strtotime(preg_replace('/(\.\d\d)\d+/', '$1', $str));
if ($ret === false) throw new Exception('Failed to parse date: ' . $str);
return $ret;
}
private function parseSettings($opts)
{
// authz_reuse: backwards compatibility to ACMECert v3.1.2 or older
if (!is_array($opts)) $opts = array('authz_reuse' => (bool)$opts);
if (!isset($opts['authz_reuse'])) $opts['authz_reuse'] = true;
$diff = array_diff_key(
$opts,
array_flip(array('authz_reuse', 'notAfter', 'notBefore', 'replaces'))
);
if (!empty($diff)) {
throw new Exception('getCertificateChain(s): Invalid option "' . key($diff) . '"');
}
return $opts;
}
private function setRFC3339Date(&$out, $key, $opts)
{
if (isset($opts[$key])) {
$out[$key] = is_string($opts[$key]) ?
$opts[$key] :
date(DATE_RFC3339, $opts[$key]);
}
}
private function makeOrder($domains, $opts)
{
$order = array(
'identifiers' => array_map(
function ($domain) {
return array('type' => 'dns', 'value' => $domain);
},
$domains
)
);
$this->setRFC3339Date($order, 'notAfter', $opts);
$this->setRFC3339Date($order, 'notBefore', $opts);
if (isset($opts['replaces'])) { // ARI
$order['replaces'] = $opts['replaces'];
$this->log('Replacing Certificate: ' . $opts['replaces']);
}
return $order;
}
private function parse_challenges($authorization, $type, &$url)
{
foreach ($authorization['challenges'] as $challenge) {
if ($challenge['type'] != $type) continue;
$url = $challenge['url'];
switch ($challenge['type']) {
case 'dns-01':
return array(
'_acme-challenge.' . $authorization['identifier']['value'],
$this->base64url(hash('sha256', $this->keyAuthorization($challenge['token']), true))
);
break;
case 'http-01':
return array(
'/.well-known/acme-challenge/' . $challenge['token'],
$this->keyAuthorization($challenge['token'])
);
break;
case 'tls-alpn-01':
return array(null, hash('sha256', $this->keyAuthorization($challenge['token'])));
break;
}
}
throw new Exception(
'Challenge type: "' . $type . '" not available, for this challenge use ' .
implode(' or ', array_map(
function ($a) {
return '"' . $a['type'] . '"';
},
$authorization['challenges']
))
);
}
private function poll($initial, $type, &$ret)
{
$max_tries = 10; // ~ 5 minutes
for ($i = 0; $i < $max_tries; $i++) {
$ret = $this->request($type);
$ret = $ret['body'];
if ($ret['status'] !== $initial) return $ret['status'] === 'valid';
$s = pow(2, min($i, 6));
if ($i !== $max_tries - 1) {
$this->log('Retrying in ' . ($s) . 's');
sleep($s);
}
}
throw new Exception('Aborted after ' . $max_tries . ' tries');
}
private function request_certificate($ret, $alternate = false)
{
$this->log('Requesting ' . ($alternate ? 'alternate' : 'default') . ' certificate-chain');
$ret = $this->request($ret['certificate'], '');
if ($ret['headers']['content-type'] !== 'application/pem-certificate-chain') {
throw new Exception('Unexpected content-type: ' . $ret['headers']['content-type']);
}
$chain = array();
foreach ($this->splitChain($ret['body']) as $cert) {
$info = $this->parseCertificate($cert);
$chain[] = '[' . $info['issuer']['CN'] . ']';
}
if (!$alternate) {
if (isset($ret['headers']['link']['alternate'])) {
$this->alternate_chains = $ret['headers']['link']['alternate'];
} else {
$this->alternate_chains = array();
}
}
$this->log(($alternate ? 'Alternate' : 'Default') . ' certificate-chain retrieved: ' . implode(' -> ', array_reverse($chain, true)));
return $ret['body'];
}
private function tmp_ssl_cnf($domains = null, $extension = '')
{
if (false === ($fn = tempnam(sys_get_temp_dir(), "CNF_"))) {
throw new Exception('Failed to create temp file !');
}
if (false === @file_put_contents(
$fn,
'HOME = .' . "\n" .
'RANDFILE=$ENV::HOME/.rnd' . "\n" .
'[v3_ca]' . "\n" .
'[req]' . "\n" .
'default_bits=2048' . "\n" .
($domains ?
'distinguished_name=req_distinguished_name' . "\n" .
'[req_distinguished_name]' . "\n" .
'[v3_req]' . "\n" .
'[SAN]' . "\n" .
'subjectAltName=' .
implode(',', array_map(function ($domain) {
return 'DNS:' . $domain;
}, $domains)) . "\n"
:
''
) . $extension
)) {
throw new Exception('Failed to write tmp file: ' . $fn);
}
return $fn;
}
private function pem2der($pem)
{
return base64_decode(implode('', array_slice(
array_map('trim', explode("\n", trim($pem))),
1,
-1
)));
}
private function make_contacts_array($contacts)
{
if (!is_array($contacts)) {
$contacts = $contacts ? array($contacts) : array();
}
return array_map(function ($contact) {
return 'mailto:' . $contact;
}, $contacts);
}
private function getTopIssuerCN($chain)
{
$tmp = $this->splitChain($chain);
$ret = $this->parseCertificate(end($tmp));
return $ret['issuer']['CN'];
}
public function splitChain($chain)
{
$delim = '-----END CERTIFICATE-----';
return array_map(function ($item) use ($delim) {
return trim($item . $delim);
}, array_filter(explode($delim, $chain), function ($item) {
return strpos($item, '-----BEGIN CERTIFICATE-----') !== false;
}));
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace app\lib\acme;
use Exception;
class ACME_Exception extends Exception
{
private $type, $subproblems;
function __construct($type, $detail, $subproblems = array())
{
$this->type = $type;
$this->subproblems = $subproblems;
parent::__construct($detail);
}
function getType()
{
return $this->type;
}
function getSubproblems()
{
return $this->subproblems;
}
}

449
app/lib/acme/ACMEv2.php Normal file
View File

@@ -0,0 +1,449 @@
<?php
namespace app\lib\acme;
use Exception;
class ACMEv2
{ // Communication with Let's Encrypt via ACME v2 protocol
protected
$ch = null, $logger = true, $bits, $sha_bits, $directory, $resources, $jwk_header, $kid_header, $account_key, $thumbprint, $nonce = null, $proxy, $proxy_config = null;
private $delay_until = null;
/**
* @param $directory string ACME directory URL
* @param $proxy int 代理模式0为不使用代理1为使用系统代理2为使用反向代理
* @param null $proxy_config array 反向代理配置proxy参数为2时必填
* @throws Exception
*/
public function __construct($directory, $proxy = 0, $proxy_config = null)
{
$this->directory = $directory;
$this->proxy = $proxy;
if ($proxy == 2) {
$this->proxy_config = $proxy_config;
}
}
public function __destruct()
{
if (PHP_MAJOR_VERSION < 8 && $this->account_key) openssl_pkey_free($this->account_key);
if ($this->ch) curl_close($this->ch);
}
public function loadAccountKey($account_key_pem)
{
if (PHP_MAJOR_VERSION < 8 && $this->account_key) openssl_pkey_free($this->account_key);
if (false === ($this->account_key = openssl_pkey_get_private($account_key_pem))) {
throw new Exception('Could not load account key: ' . $account_key_pem . ' (' . $this->get_openssl_error() . ')');
}
if (false === ($details = openssl_pkey_get_details($this->account_key))) {
throw new Exception('Could not get account key details: ' . $account_key_pem . ' (' . $this->get_openssl_error() . ')');
}
$this->bits = $details['bits'];
switch ($details['type']) {
case OPENSSL_KEYTYPE_EC:
if (version_compare(PHP_VERSION, '7.1.0') < 0) throw new Exception('PHP >= 7.1.0 required for EC keys !');
$this->sha_bits = ($this->bits == 521 ? 512 : $this->bits);
$this->jwk_header = array( // JOSE Header - RFC7515
'alg' => 'ES' . $this->sha_bits,
'jwk' => array( // JSON Web Key
'crv' => 'P-' . $details['bits'],
'kty' => 'EC',
'x' => $this->base64url(str_pad($details['ec']['x'], ceil($this->bits / 8), "\x00", STR_PAD_LEFT)),
'y' => $this->base64url(str_pad($details['ec']['y'], ceil($this->bits / 8), "\x00", STR_PAD_LEFT))
)
);
break;
case OPENSSL_KEYTYPE_RSA:
$this->sha_bits = 256;
$this->jwk_header = array( // JOSE Header - RFC7515
'alg' => 'RS256',
'jwk' => array( // JSON Web Key
'e' => $this->base64url($details['rsa']['e']), // public exponent
'kty' => 'RSA',
'n' => $this->base64url($details['rsa']['n']) // public modulus
)
);
break;
default:
throw new Exception('Unsupported key type! Must be RSA or EC key.');
break;
}
$this->kid_header = array(
'alg' => $this->jwk_header['alg'],
'kid' => null
);
$this->thumbprint = $this->base64url( // JSON Web Key (JWK) Thumbprint - RFC7638
hash(
'sha256',
json_encode($this->jwk_header['jwk']),
true
)
);
}
public function getAccountID()
{
if (!$this->kid_header['kid']) self::getAccount();
return $this->kid_header['kid'];
}
public function setLogger($value = true)
{
switch (true) {
case is_bool($value):
break;
case is_callable($value):
break;
default:
throw new Exception('setLogger: invalid value provided');
break;
}
$this->logger = $value;
}
public function log($txt)
{
switch (true) {
case $this->logger === true:
error_log($txt);
break;
case $this->logger === false:
break;
default:
$fn = $this->logger;
$fn($txt);
break;
}
}
protected function create_ACME_Exception($type, $detail, $subproblems = array())
{
$this->log('ACME_Exception: ' . $detail . ' (' . $type . ')');
return new ACME_Exception($type, $detail, $subproblems);
}
protected function get_openssl_error()
{
$out = array();
$arr = error_get_last();
if (is_array($arr)) {
$out[] = $arr['message'];
}
$out[] = openssl_error_string();
return implode(' | ', $out);
}
protected function getAccount()
{
$this->log('Getting account info');
$ret = $this->request('newAccount', array('onlyReturnExisting' => true));
$this->log('Account info retrieved');
return $ret;
}
protected function keyAuthorization($token)
{
return $token . '.' . $this->thumbprint;
}
protected function readDirectory()
{
$this->log('Initializing ACME v2 environment: ' . $this->directory);
$ret = $this->http_request($this->directory); // Read ACME Directory
if (
!is_array($ret['body']) ||
!empty(array_diff_key(
array_flip(array('newNonce', 'newAccount', 'newOrder')),
$ret['body']
))
) {
throw new Exception('Failed to read directory: ' . $this->directory);
}
$this->resources = $ret['body']; // store resources for later use
$this->log('Initialized');
}
protected function request($type, $payload = '', $retry = false)
{
if (!$this->jwk_header) {
throw new Exception('use loadAccountKey to load an account key');
}
if (!$this->resources) $this->readDirectory();
if (0 === stripos($type, 'http')) {
$this->resources['_tmp'] = $type;
$type = '_tmp';
}
try {
$ret = $this->http_request($this->resources[$type], json_encode(
$this->jws_encapsulate($type, $payload)
));
} catch (ACME_Exception $e) { // retry previous request once, if replay-nonce expired/failed
if (!$retry && $e->getType() === 'urn:ietf:params:acme:error:badNonce') {
$this->log('Replay-Nonce expired, retrying previous request');
return $this->request($type, $payload, true);
}
if (!$retry && $e->getType() === 'urn:ietf:params:acme:error:rateLimited' && $this->delay_until !== null) {
return $this->request($type, $payload, true);
}
throw $e; // rethrow all other exceptions
}
if (!$this->kid_header['kid'] && $type === 'newAccount') {
// 反向替换反向代理配置,防止破坏签名
$this->kid_header['kid'] = $this->unproxiedURL($ret['headers']['location']);
$this->log('AccountID: ' . $this->kid_header['kid']);
}
return $ret;
}
protected function jws_encapsulate($type, $payload, $is_inner_jws = false)
{ // RFC7515
if ($type === 'newAccount' || $is_inner_jws) {
$protected = $this->jwk_header;
} else {
$this->getAccountID();
$protected = $this->kid_header;
}
if (!$is_inner_jws) {
if (!$this->nonce) {
$ret = $this->http_request($this->resources['newNonce'], false);
}
$protected['nonce'] = $this->nonce;
$this->nonce = null;
}
if (!isset($this->resources[$type])) {
throw new Exception('Resource "' . $type . '" not available.');
}
// 反向替换反向代理配置,防止破坏签名
$protected['url'] = $this->unproxiedURL($this->resources[$type]);
$protected64 = $this->base64url(json_encode($protected, JSON_UNESCAPED_SLASHES));
$payload64 = $this->base64url(is_string($payload) ? $payload : json_encode($payload, JSON_UNESCAPED_SLASHES));
if (false === openssl_sign(
$protected64 . '.' . $payload64,
$signature,
$this->account_key,
'SHA' . $this->sha_bits
)) {
throw new Exception('Failed to sign payload !' . ' (' . $this->get_openssl_error() . ')');
}
return array(
'protected' => $protected64,
'payload' => $payload64,
'signature' => $this->base64url($this->jwk_header['alg'][0] == 'R' ? $signature : $this->asn2signature($signature, ceil($this->bits / 8)))
);
}
private function asn2signature($asn, $pad_len)
{
if ($asn[0] !== "\x30") throw new Exception('ASN.1 SEQUENCE not found !');
$asn = substr($asn, $asn[1] === "\x81" ? 3 : 2);
if ($asn[0] !== "\x02") throw new Exception('ASN.1 INTEGER 1 not found !');
$R = ltrim(substr($asn, 2, ord($asn[1])), "\x00");
$asn = substr($asn, ord($asn[1]) + 2);
if ($asn[0] !== "\x02") throw new Exception('ASN.1 INTEGER 2 not found !');
$S = ltrim(substr($asn, 2, ord($asn[1])), "\x00");
return str_pad($R, $pad_len, "\x00", STR_PAD_LEFT) . str_pad($S, $pad_len, "\x00", STR_PAD_LEFT);
}
protected function base64url($data)
{ // RFC7515 - Appendix C
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
protected function base64url_decode($data)
{
return base64_decode(strtr($data, '-_', '+/'));
}
private function json_decode($str)
{
$ret = json_decode($str, true);
if ($ret === null) {
throw new Exception('Could not parse JSON: ' . $str);
}
return $ret;
}
protected function http_request($url, $data = null)
{
if ($this->ch === null) {
$this->ch = curl_init();
}
if ($this->delay_until !== null) {
$delta = $this->delay_until - time();
if ($delta > 0) {
$this->log('Delaying ' . $delta . 's (rate limit)');
sleep($delta);
}
$this->delay_until = null;
}
// 替换反向代理配置
$url = $this->proxiedURL($url);
$method = $data === false ? 'HEAD' : ($data === null ? 'GET' : 'POST');
$user_agent = 'ACMECert v3.4.0 (+https://github.com/skoerfgen/ACMECert)';
$header = ($data === null || $data === false) ? array() : array('Content-Type: application/jose+json');
$headers = array();
curl_setopt_array($this->ch, array(
CURLOPT_URL => $url,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_NOBODY => $data === false,
CURLOPT_USERAGENT => $user_agent,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $header,
CURLOPT_POSTFIELDS => $data,
CURLOPT_HEADERFUNCTION => static function ($ch, $header) use (&$headers) {
$headers[] = $header;
return strlen($header);
}
));
if ($this->proxy == 1) {
curl_set_proxy($this->ch);
}
$took = microtime(true);
$body = curl_exec($this->ch);
$took = round(microtime(true) - $took, 2) . 's';
if ($body === false) throw new Exception('HTTP Request Error: ' . curl_error($this->ch));
$headers = array_reduce( // parse http response headers into array
array_filter($headers, function ($item) {
return trim($item) != '';
}),
function ($carry, $item) use (&$code) {
$parts = explode(':', $item, 2);
if (count($parts) === 1) {
list(, $code) = explode(' ', trim($item), 3);
$carry = array();
} else {
list($k, $v) = $parts;
$k = strtolower(trim($k));
switch ($k) {
case 'link':
if (preg_match('/<(.*)>\s*;\s*rel=\"(.*)\"/', $v, $matches)) {
$carry[$k][$matches[2]][] = trim($matches[1]);
}
break;
case 'content-type':
list($v) = explode(';', $v, 2);
default:
$carry[$k] = trim($v);
break;
}
}
return $carry;
},
array()
);
$this->log(' ' . $url . ' [' . $code . '] (' . $took . ')');
if (!empty($headers['replay-nonce'])) $this->nonce = $headers['replay-nonce'];
if (isset($headers['retry-after'])) {
if (is_numeric($headers['retry-after'])) {
$this->delay_until = time() + ceil($headers['retry-after']);
} else {
$this->delay_until = strtotime($headers['retry-after']);
}
$tmp = $this->delay_until - time();
// ignore delay if not in range 1s..5min
if ($tmp > 300 || $tmp < 1) $this->delay_until = null;
}
if (!empty($headers['content-type'])) {
switch ($headers['content-type']) {
case 'application/json':
if ($code[0] == '2') { // on non 2xx response: fall through to problem+json case
$body = $this->json_decode($body);
if (isset($body['error']) && !(isset($body['status']) && $body['status'] === 'valid')) {
$this->handleError($body['error']);
}
break;
}
case 'application/problem+json':
$body = $this->json_decode($body);
$this->handleError($body);
break;
}
}
if ($code[0] != '2') {
throw new Exception('Invalid HTTP-Status-Code received: ' . $code . ': ' . print_r($body, true));
}
$ret = array(
'code' => $code,
'headers' => $headers,
'body' => $body
);
return $ret;
}
private function handleError($error)
{
throw $this->create_ACME_Exception(
$error['type'],
$error['detail'],
array_map(function ($subproblem) {
return $this->create_ACME_Exception(
$subproblem['type'],
(isset($subproblem['identifier']['value']) ?
'"' . $subproblem['identifier']['value'] . '": ' :
''
) . $subproblem['detail']
);
}, isset($error['subproblems']) ? $error['subproblems'] : array())
);
}
// 替换反向代理配置
protected function proxiedURL($url)
{
if ($this->proxy == 2) {
return str_replace(
$this->proxy_config['origin'],
$this->proxy_config['proxy'],
$url
);
}
return $url;
}
// 反向替换反向代理配置
protected function unproxiedURL($url)
{
if ($this->proxy == 2) {
return str_replace(
$this->proxy_config['proxy'],
$this->proxy_config['origin'],
$url
);
}
return $url;
}
}

168
app/lib/cert/aliyun.php Normal file
View File

@@ -0,0 +1,168 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\client\Aliyun as AliyunClient;
use Exception;
class aliyun implements CertInterface
{
private $AccessKeyId;
private $AccessKeySecret;
private $Endpoint = 'cas.aliyuncs.com'; //API接入域名
private $Version = '2020-04-07'; //API版本号
private $config;
private $logger;
private AliyunClient $client;
public function __construct($config, $ext = null)
{
$this->AccessKeyId = $config['AccessKeyId'];
$this->AccessKeySecret = $config['AccessKeySecret'];
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $this->Endpoint, $this->Version, $proxy);
$this->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;
}
}

114
app/lib/cert/customacme.php Normal file
View File

@@ -0,0 +1,114 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\acme\ACMECert;
use Exception;
class customacme implements CertInterface
{
private $ac;
private $config;
private $ext;
public function __construct($config, $ext = null)
{
$this->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);
}
}

142
app/lib/cert/google.php Normal file
View File

@@ -0,0 +1,142 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\acme\ACMECert;
use Exception;
class google implements CertInterface
{
private $directories = array(
'live' => 'https://dv.acme-v02.api.pki.goog',
'staging' => 'https://dv.acme-v02.test-api.pki.goog'
);
private $ac;
private $config;
private $ext;
public function __construct($config, $ext = null)
{
$this->config = $config;
if (empty($config['mode'])) $config['mode'] = 'live';
if (empty($config['proxy_url'])) $config['proxy_url'] = '';
$this->ac = new ACMECert($this->directories[$config['mode']] . '/directory', (int)$config['proxy'], [
'origin' => $this->directories[$config['mode']],
'proxy' => rtrim($config['proxy_url'], '/'),
]);
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();
} 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 "pki.goog"'];
}*/
$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()
{
$api = "https://gts.rat.dev/eab";
$response = http_request($api, null, null, null, null, $this->config['proxy'] == 1, 'GET', 10);
$result = json_decode($response['body'], true);
if (!isset($result['msg'])) {
throw new Exception('解析返回数据失败:' . $response['body']);
} elseif ($result['msg'] != 'success') {
throw new Exception('获取EAB失败' . $result['msg']);
} elseif (empty($result['data']['key_id']) || empty($result['data']['mac_key'])) {
throw new Exception('获取EAB失败返回数据不完整');
}
return ['kid' => $result['data']['key_id'], 'key' => $result['data']['mac_key']];
}
}

164
app/lib/cert/huoshan.php Normal file
View File

@@ -0,0 +1,164 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\client\Volcengine;
use Exception;
class huoshan implements CertInterface
{
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint = "open.volcengineapi.com";
private $service = "certificate_service";
private $version = "2021-06-01";
private $region = "cn-north-1";
private $logger;
private Volcengine $client;
public function __construct($config = null, $ext = null)
{
$this->AccessKeyId = $config['AccessKeyId'];
$this->SecretAccessKey = $config['SecretAccessKey'];
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $this->service, $this->version, $this->region, $proxy);
}
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;
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\acme\ACMECert;
use Exception;
class letsencrypt implements CertInterface
{
private $directories = array(
'live' => '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);
}
}

200
app/lib/cert/tencent.php Normal file
View File

@@ -0,0 +1,200 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\client\TencentCloud;
use Exception;
class tencent implements CertInterface
{
private $SecretId;
private $SecretKey;
private $email;
private $endpoint = "ssl.tencentcloudapi.com";
private $service = "ssl";
private $version = "2019-12-05";
private $logger;
private $proxy;
private TencentCloud $client;
public function __construct($config, $ext = null)
{
$this->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;
}
}

188
app/lib/cert/ucloud.php Normal file
View File

@@ -0,0 +1,188 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\client\Ucloud as UcloudClient;
use Exception;
class ucloud implements CertInterface
{
private $PublicKey;
private $PrivateKey;
private $config;
private $logger;
private UcloudClient $client;
public function __construct($config, $ext = null)
{
$this->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;
}
}

134
app/lib/cert/zerossl.php Normal file
View File

@@ -0,0 +1,134 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\acme\ACMECert;
use Exception;
class zerossl implements CertInterface
{
private $directory = 'https://acme.zerossl.com/v2/DV90';
private $ac;
private $config;
private $ext;
public function __construct($config, $ext = null)
{
$this->config = $config;
$this->ac = new ACMECert($this->directory, (int)$config['proxy']);
if ($ext) {
$this->ext = $ext;
$this->ac->loadAccountKey($ext['key']);
$this->ac->setAccount($ext['kid']);
}
}
public function register()
{
if (empty($this->config['email'])) throw new Exception('邮件地址不能为空');
if (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']];
}
}

364
app/lib/client/AWS.php Normal file
View File

@@ -0,0 +1,364 @@
<?php
namespace app\lib\client;
use Exception;
/**
* AWS
*/
class AWS
{
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint;
private $service;
private $version;
private $region;
private $etag;
private $proxy = false;
public function __construct($AccessKeyId, $SecretAccessKey, $endpoint, $service, $version, $region, $proxy = false)
{
$this->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('<root/>');
}
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();
}
}

101
app/lib/client/Aliyun.php Normal file
View File

@@ -0,0 +1,101 @@
<?php
namespace app\lib\client;
use Exception;
/**
* 阿里云
*/
class Aliyun
{
private $AccessKeyId;
private $AccessKeySecret;
private $Endpoint;
private $Version;
private $proxy = false;
public function __construct($AccessKeyId, $AccessKeySecret, $Endpoint, $Version, $proxy = false)
{
$this->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));
}
}

View File

@@ -0,0 +1,188 @@
<?php
namespace app\lib\client;
use Exception;
/**
* 阿里云V3
*/
class AliyunNew
{
private $AccessKeyId;
private $AccessKeySecret;
private $Endpoint;
private $Version;
private $proxy = false;
public function __construct($AccessKeyId, $AccessKeySecret, $Endpoint, $Version, $proxy = false)
{
$this->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('返回数据解析失败');
}
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace app\lib\client;
use Exception;
class AliyunOSS
{
private $AccessKeyId;
private $AccessKeySecret;
private $Endpoint;
private $proxy = false;
public function __construct($AccessKeyId, $AccessKeySecret, $Endpoint, $proxy = false)
{
$this->AccessKeyId = $AccessKeyId;
$this->AccessKeySecret = $AccessKeySecret;
$this->Endpoint = $Endpoint;
$this->proxy = $proxy;
}
public function addBucketCnameCert($bucket, $domain, $cert_id)
{
$strXml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<BucketCnameConfiguration>
</BucketCnameConfiguration>
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 version="1.0" encoding="utf-8"?>
<BucketCnameConfiguration>
</BucketCnameConfiguration>
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"
);
}

View File

@@ -0,0 +1,181 @@
<?php
namespace app\lib\client;
use Exception;
/**
* 百度云
*/
class BaiduCloud
{
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint;
private $proxy = false;
public function __construct($AccessKeyId, $SecretAccessKey, $endpoint, $proxy = false)
{
$this->AccessKeyId = $AccessKeyId;
$this->SecretAccessKey = $SecretAccessKey;
$this->endpoint = $endpoint;
$this->proxy = $proxy;
}
/**
* @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('返回数据解析失败');
}
}
}

164
app/lib/client/Ctyun.php Normal file
View File

@@ -0,0 +1,164 @@
<?php
namespace app\lib\client;
use Exception;
/**
* 天翼云
*/
class Ctyun
{
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint;
private $proxy = false;
public function __construct($AccessKeyId, $SecretAccessKey, $endpoint, $proxy = false)
{
$this->AccessKeyId = $AccessKeyId;
$this->SecretAccessKey = $SecretAccessKey;
$this->endpoint = $endpoint;
$this->proxy = $proxy;
}
/**
* @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('返回数据解析失败');
}
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace app\lib\client;
use Exception;
/**
* 华为云
*/
class HuaweiCloud
{
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint;
private $proxy = false;
public function __construct($AccessKeyId, $SecretAccessKey, $endpoint, $proxy = false)
{
$this->AccessKeyId = $AccessKeyId;
$this->SecretAccessKey = $SecretAccessKey;
$this->endpoint = $endpoint;
$this->proxy = $proxy;
}
/**
* @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('返回数据解析失败');
}
}
}
}

191
app/lib/client/Jdcloud.php Normal file
View File

@@ -0,0 +1,191 @@
<?php
namespace app\lib\client;
use Exception;
/**
* 京东云
*/
class Jdcloud
{
private static $algorithm = 'JDCLOUD2-HMAC-SHA256';
private $AccessKeyId;
private $AccessKeySecret;
private $endpoint;
private $service;
private $region;
private $proxy = false;
public function __construct($AccessKeyId, $AccessKeySecret, $endpoint, $service, $region, $proxy = false)
{
$this->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 . ')');
}
}
}
}

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

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

143
app/lib/client/Qiniu.php Normal file
View File

@@ -0,0 +1,143 @@
<?php
namespace app\lib\client;
use Exception;
/**
* 七牛云
*/
class Qiniu
{
private $ApiUrl = 'https://api.qiniu.com';
private $AccessKey;
private $SecretKey;
private $proxy = false;
public function __construct($AccessKey, $SecretKey, $proxy = false)
{
$this->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('返回数据解析失败');
}
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace app\lib\client;
use Exception;
/**
* 腾讯云
*/
class TencentCloud
{
private $SecretId;
private $SecretKey;
private $endpoint;
private $service;
private $version;
private $region;
private $proxy = false;
public function __construct($SecretId, $SecretKey, $endpoint, $service, $version, $region = null, $proxy = false)
{
$this->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('返回数据解析失败');
}
}
}

51
app/lib/client/Ucloud.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
namespace app\lib\client;
use Exception;
class Ucloud
{
const VERSION = "0.1.0";
private $ApiUrl = 'https://api.ucloud.cn/';
private $PublicKey;
private $PrivateKey;
public function __construct($PublicKey, $PrivateKey)
{
$this->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);
}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace app\lib\client;
use Exception;
/**
* 火山引擎
*/
class Volcengine
{
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint = "open.volcengineapi.com";
private $service;
private $version;
private $region;
private $proxy = false;
public function __construct($AccessKeyId, $SecretAccessKey, $endpoint, $service, $version, $region, $proxy = false)
{
$this->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 . ')');
}
}
}
}

735
app/lib/deploy/aliyun.php Normal file
View File

@@ -0,0 +1,735 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\Aliyun as AliyunClient;
use app\lib\client\AliyunNew as AliyunNewClient;
use app\lib\client\AliyunOSS;
use Exception;
class aliyun implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $AccessKeySecret;
private $proxy;
public function __construct($config)
{
$this->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);
}
}
}

150
app/lib/deploy/aws.php Normal file
View File

@@ -0,0 +1,150 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\AWS as AWSClient;
use Exception;
class aws implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $SecretAccessKey;
private $proxy;
public function __construct($config)
{
$this->AccessKeyId = $config['AccessKeyId'];
$this->SecretAccessKey = $config['SecretAccessKey'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function check()
{
if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空');
$client = new 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('<DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/"></DistributionConfig>');
$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);
}
}
}

160
app/lib/deploy/baidu.php Normal file
View File

@@ -0,0 +1,160 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\BaiduCloud;
use Exception;
class baidu implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $SecretAccessKey;
private $proxy;
public function __construct($config)
{
$this->AccessKeyId = $config['AccessKeyId'];
$this->SecretAccessKey = $config['SecretAccessKey'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function check()
{
if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空');
$client = new 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);
}
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class baishan implements DeployInterface
{
private $logger;
private $url = 'https://cdn.api.baishan.com';
private $token;
private $proxy;
public function __construct($config)
{
$this->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);
}
}
}

335
app/lib/deploy/btpanel.php Normal file
View File

@@ -0,0 +1,335 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\CertHelper;
use Exception;
class btpanel implements DeployInterface
{
private $logger;
private $url;
private $key;
private $version;
private $proxy;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->key = $config['key'];
$this->version = isset($config['version']) ? intval($config['version']) : 0;
$this->proxy = $config['proxy'] == 1;
}
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'];
}
}

158
app/lib/deploy/btwaf.php Normal file
View File

@@ -0,0 +1,158 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class btwaf implements DeployInterface
{
private $logger;
private $url;
private $key;
private $proxy;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->key = $config['key'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥');
$path = '/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'];
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class cachefly implements DeployInterface
{
private $logger;
private $url = 'https://api.cachefly.com/api/2.5';
private $apikey;
private $proxy;
public function __construct($config)
{
$this->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);
}
}
}

124
app/lib/deploy/cdnfly.php Normal file
View File

@@ -0,0 +1,124 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class cdnfly implements DeployInterface
{
private $logger;
private $url;
private $api_key;
private $api_secret;
private $auth = 0;
private $username;
private $password;
private $proxy;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->api_key = $config['api_key'];
$this->api_secret = $config['api_secret'];
$this->auth = isset($config['auth']) ? $config['auth'] : 0;
if ($this->auth == 1) {
$this->username = $config['username'];
$this->password = $config['password'];
}
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if ($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);
}
}
}

184
app/lib/deploy/ctyun.php Normal file
View File

@@ -0,0 +1,184 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\Ctyun as CtyunClient;
use Exception;
class ctyun implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $SecretAccessKey;
private $proxy;
public function __construct($config)
{
$this->AccessKeyId = $config['AccessKeyId'];
$this->SecretAccessKey = $config['SecretAccessKey'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function check()
{
if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空');
$client = new 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);
}
}
}

124
app/lib/deploy/doge.php Normal file
View File

@@ -0,0 +1,124 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class doge implements DeployInterface
{
private $logger;
private $AccessKey;
private $SecretKey;
private $proxy;
public function __construct($config)
{
$this->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);
}
}
}

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

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

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

@@ -0,0 +1,113 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class ftp implements DeployInterface
{
private $logger;
private $config;
public function __construct($config)
{
$this->config = $config;
}
public function check()
{
$this->connect();
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$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;
}
}

77
app/lib/deploy/gcore.php Normal file
View File

@@ -0,0 +1,77 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class gcore implements DeployInterface
{
private $logger;
private $url = 'https://api.gcore.com';
private $apikey;
private $proxy;
public function __construct($config)
{
$this->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);
}
}
}

154
app/lib/deploy/goedge.php Normal file
View File

@@ -0,0 +1,154 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class goedge implements DeployInterface
{
private $logger;
private $url;
private $accessKeyId;
private $accessKey;
private $usertype;
private $systype;
private $proxy;
private $accessToken;
public function __construct($config)
{
$this->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);
}
}
}

152
app/lib/deploy/huawei.php Normal file
View File

@@ -0,0 +1,152 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\HuaweiCloud;
use Exception;
class huawei implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $SecretAccessKey;
private $proxy;
public function __construct($config)
{
$this->AccessKeyId = $config['AccessKeyId'];
$this->SecretAccessKey = $config['SecretAccessKey'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function check()
{
if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空');
$client = new 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);
}
}
}

228
app/lib/deploy/huoshan.php Normal file
View File

@@ -0,0 +1,228 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\Volcengine;
use Exception;
class huoshan implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $SecretAccessKey;
private $proxy;
public function __construct($config)
{
$this->AccessKeyId = $config['AccessKeyId'];
$this->SecretAccessKey = $config['SecretAccessKey'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function check()
{
if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空');
$client = new 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);
}
}
}

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

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

208
app/lib/deploy/kangle.php Normal file
View File

@@ -0,0 +1,208 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class kangle implements DeployInterface
{
private $logger;
private $url;
private $auth;
private $username;
private $password;
private $skey;
private $proxy;
private $cookie;
public function __construct($config)
{
$this->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);
}
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class kangleadmin implements DeployInterface
{
private $logger;
private $url;
private $path;
private $username;
private $skey;
private $proxy;
private $cookie;
public function __construct($config)
{
$this->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);
}
}
}

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

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

94
app/lib/deploy/kuocai.php Normal file
View File

@@ -0,0 +1,94 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class kuocai implements DeployInterface
{
private $logger;
private $username;
private $password;
private $proxy;
private $token = null;
public function __construct($config)
{
$this->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->request('/login/loginUser', [
'userAccount' => $this->username,
'userPwd' => $this->password,
'remember' => 'true'
]);
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$id = $config['id'];
if (empty($id)) {
throw new Exception('域名ID不能为空');
}
$this->token = $this->request('/login/loginUser', [
'userAccount' => $this->username,
'userPwd' => $this->password,
'remember' => 'true'
]);
$this->request('/CdnDomainHttps/httpsConfiguration', [
'doMainId' => $id,
'https' => [
'certificate_name' => uniqid('cert_'),
'certificate_source' => '0',
'certificate_value' => $fullchain,
'https_status' => 'on',
'private_key' => $privatekey,
]
], true);
$this->log("域名ID:{$id}更新成功!");
}
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, $json = false)
{
$url = 'https://kuocai.cn' . $path;
$body = $json ? json_encode($params) : $params;
$headers = [];
if ($json) $headers['Content-Type'] = 'application/json';
$response = http_request(
$url,
$body,
null,
$this->token ? "kuocai_cdn_token={$this->token}" : null,
$headers,
$this->proxy
);
$result = json_decode($response['body'], true);
if (isset($result['code']) && $result['code'] == 'SUCCESS') {
return isset($result['data']) ? $result['data'] : null;
} elseif (isset($result['message'])) {
throw new Exception($result['message']);
} else {
throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
}
}
}

122
app/lib/deploy/lecdn.php Normal file
View File

@@ -0,0 +1,122 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class lecdn implements DeployInterface
{
private $logger;
private $url;
private $email;
private $password;
private $auth;
private $apiKey;
private $proxy;
private $accessToken;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->email = $config['email'];
$this->password = $config['password'];
$this->auth = isset($config['auth']) ? intval($config['auth']) : 0;
if ($this->auth == 1) {
$this->apiKey = $config['api_key'];
}
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if ($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);
}
}
}

77
app/lib/deploy/local.php Normal file
View File

@@ -0,0 +1,77 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class local implements DeployInterface
{
private $logger;
public function __construct($config) {}
public function check() {}
public function deploy($fullchain, $privatekey, $config, &$info)
{
if (!empty($config['cmd']) && !function_exists('exec')) {
throw new Exception('exec函数被禁用');
}
if ($config['format'] == 'pem') {
$dir = dirname($config['pem_cert_file']);
if (!is_dir($dir)) throw new Exception($dir . ' 目录不存在');
if (!is_writable($dir)) throw new Exception($dir . ' 目录不可写');
if (file_put_contents($config['pem_cert_file'], $fullchain)) {
$this->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;
}
}

114
app/lib/deploy/lucky.php Normal file
View File

@@ -0,0 +1,114 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class lucky implements DeployInterface
{
private $logger;
private $url;
private $opentoken;
private $proxy;
public function __construct($config)
{
$this->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'] . ')');
}
}
}

127
app/lib/deploy/mwpanel.php Normal file
View File

@@ -0,0 +1,127 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class mwpanel implements DeployInterface
{
private $logger;
private $url;
private $appid;
private $appsecret;
private $proxy;
public function __construct($config)
{
$this->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'];
}
}

162
app/lib/deploy/opanel.php Normal file
View File

@@ -0,0 +1,162 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class opanel implements DeployInterface
{
private $logger;
private $url;
private $key;
private $proxy;
private $nodeName;
public function __construct($config)
{
$this->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)
{
if (isset($config['type']) && $config['type'] == '3') {
$params = [
'cert' => $fullchain,
'key' => $privatekey,
'ssl' => 'Enable',
'sslID' => null,
'sslType' => 'import-paste',
];
try {
$this->request('/core/settings/ssl/update', $params);
$this->log("面板证书更新成功!");
return;
} catch (Exception $e) {
throw new Exception("面板证书更新失败:" . $e->getMessage());
}
}
if (isset($config['node_name'])) $this->nodeName = $config['node_name'];
if (!empty($config['id'])) {
$params = [
'sslID' => intval($config['id']),
'type' => 'paste',
'certificate' => $fullchain,
'privateKey' => $privatekey,
'description' => '',
];
try {
$this->request('/websites/ssl/upload', $params);
$this->log("证书ID:{$config['id']}更新成功!");
return;
} catch (Exception $e) {
throw new Exception("证书ID:{$config['id']}更新失败:" . $e->getMessage());
}
}
$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) {
$params = [
'sslID' => 0,
'type' => 'paste',
'certificate' => $fullchain,
'privateKey' => $privatekey,
'description' => '',
];
$this->request('/websites/ssl/upload', $params);
$this->log("证书上传成功!");
}
}
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,
];
if (!empty($this->nodeName)) {
$headers['CurrentNode'] = $this->nodeName;
}
$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'] . ')');
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class proxmox implements DeployInterface
{
private $logger;
private $url;
private $api_user;
private $api_key;
private $proxy;
public function __construct($config)
{
$this->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);
}
}
}

159
app/lib/deploy/qiniu.php Normal file
View File

@@ -0,0 +1,159 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\Qiniu as QiniuClient;
use Exception;
class qiniu implements DeployInterface
{
private $logger;
private $AccessKey;
private $SecretKey;
private QiniuClient $client;
public function __construct($config)
{
$this->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);
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class rainyun implements DeployInterface
{
private $logger;
private $url = 'https://api.v2.rainyun.com';
private $apikey;
private $proxy;
public function __construct($config)
{
$this->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);
}
}
}

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

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

112
app/lib/deploy/safeline.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class safeline implements DeployInterface
{
private $logger;
private $url;
private $token;
private $proxy;
public function __construct($config)
{
$this->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);
}
}
}

216
app/lib/deploy/ssh.php Normal file
View File

@@ -0,0 +1,216 @@
<?php
namespace app\lib\deploy;
use app\lib\CertHelper;
use app\lib\DeployInterface;
use Exception;
class ssh implements DeployInterface
{
private $logger;
private $config;
public function __construct($config)
{
$this->config = $config;
}
public function check()
{
$this->connect();
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$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_pass = $config['pfx_pass'] ?? null;
$pfx = CertHelper::getPfx($fullchain, $privatekey, $pfx_pass);
$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);
$passphrase = $this->config['passphrase'] ?? null; // 私钥密码
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $passphrase)) {
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;
}
}

167
app/lib/deploy/synology.php Normal file
View File

@@ -0,0 +1,167 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class synology implements DeployInterface
{
private $logger;
private $url;
private $username;
private $password;
private $version;
private $token;
private $proxy;
public function __construct($config)
{
$this->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);
}
}
}

295
app/lib/deploy/tencent.php Normal file
View File

@@ -0,0 +1,295 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\TencentCloud;
use Exception;
class tencent implements DeployInterface
{
private $logger;
private $SecretId;
private $SecretKey;
private TencentCloud $client;
private $proxy;
public function __construct($config)
{
$this->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);
}
}
}

118
app/lib/deploy/ucloud.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\Ucloud as UcloudClient;
use Exception;
class ucloud implements DeployInterface
{
private $logger;
private $PublicKey;
private $PrivateKey;
private UcloudClient $client;
public function __construct($config)
{
$this->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);
}
}
}

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

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

133
app/lib/deploy/upyun.php Normal file
View File

@@ -0,0 +1,133 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class upyun implements DeployInterface
{
private $logger;
private $username;
private $password;
private $proxy;
private $cookie;
public function __construct($config)
{
$this->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);
}
}
}

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

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

474
app/lib/deploy/wangsu.php Normal file
View File

@@ -0,0 +1,474 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class wangsu implements DeployInterface
{
private $logger;
private $username;
private $apiKey;
private $spKey;
private $proxy;
public function __construct($config)
{
$this->username = $config['username'];
$this->apiKey = $config['apiKey'];
$this->spKey = $config['spKey'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function check()
{
if (empty($this->username) || empty($this->apiKey)) throw new Exception('必填参数不能为空');
$this->request('/api/ssl/certificate');
return true;
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
if ($config['product'] == 'cdnpro') {
$this->deploy_cdnpro($fullchain, $privatekey, $config, $info);
} elseif ($config['product'] == 'cdn') {
$this->deploy_cdn($fullchain, $privatekey, $config, $info);
} elseif ($config['product'] == 'certificate') {
$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']);
$this->get_cert_id($fullchain, $privatekey, $cert_name, $config['cert_id'], $serial_no, true);
} else {
throw new Exception('未知的产品类型');
}
}
public function deploy_cdn($fullchain, $privatekey, $config, &$info)
{
if (empty($config['domains'])) {
throw new Exception('绑定的域名不能为空');
}
$domains = explode(',', $config['domains']);
$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']);
$this->log('证书序列号:' . $serial_no);
$cert_id = isset($info['cert_id']) ? $info['cert_id'] : null;
$cert_id = $this->get_cert_id($fullchain, $privatekey, $cert_name, $cert_id, $serial_no, false);
$param = [
'certificateId' => $cert_id,
'domainNames' => $domains
];
try {
$data = $this->request('/api/config/certificate/batch', $param, true, null, 'PUT');
} catch (Exception $e) {
throw new Exception('绑定域名失败:' . $e->getMessage());
}
$this->log('绑定证书成功证书ID' . $cert_id);
$info['cert_id'] = $cert_id;
}
public function deploy_cdnpro($fullchain, $privatekey, $config, &$info)
{
if (empty($config['domain'])) {
throw new Exception('绑定的域名不能为空');
}
$domain = $config['domain'];
$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_cdnpro($fullchain, $privatekey, $cert_name);
try {
$hostnameInfo = $this->request('/cdn/hostnames/' . $domain);
} catch (Exception $e) {
throw new Exception('获取域名信息失败:' . $e->getMessage());
}
if (empty($hostnameInfo["propertyInProduction"])) {
throw new Exception('域名 ' . $domain . ' 不存在或未部署到生产环境');
} else {
$this->log('CDN域名 ' . $domain . ' 对应的加速项目ID' . $hostnameInfo["propertyInProduction"]["propertyId"]);
$this->log('CDN域名 ' . $domain . ' 对应的加速项目生产版本:' . $hostnameInfo["propertyInProduction"]["version"]);
}
if ($hostnameInfo["propertyInProduction"]["certificateId"] == $cert_id) {
$this->log('CDN域名 ' . $domain . ' 已绑定证书:' . $cert_name);
return;
}
try {
$properity = $this->request('/cdn/properties/' . $hostnameInfo["propertyInProduction"]["propertyId"] . '/versions/' . $hostnameInfo["propertyInProduction"]["version"]);
} catch (Exception $e) {
throw new Exception('获取加速项目版本信息失败:' . $e->getMessage());
}
$properityConfig = $properity["configs"];
$properityConfig["tlsCertificateId"] = $cert_id;
try {
$data = $this->request('/cdn/properties/' . $hostnameInfo["propertyInProduction"]["propertyId"] . '/versions', $properityConfig, true);
} catch (Exception $e) {
throw new Exception('新增加速项目版本失败:' . $e->getMessage());
}
$url_parts = parse_url($data);
$path_parts = explode('/', $url_parts['path']);
$newVersion = end($path_parts);
$param = [
'propertyId' => $hostnameInfo["propertyInProduction"]["propertyId"],
'version' => intval($newVersion),
];
try {
$data = $this->request('/cdn/validations', $param, true);
} catch (Exception $e) {
throw new Exception('发起加速项目验证失败:' . $e->getMessage());
}
$url_parts = parse_url($data);
$path_parts = explode('/', $url_parts['path']);
$validationTaskId = end($path_parts);
$this->log('验证任务ID' . $validationTaskId);
$attempts = 0;
$maxAttempts = 12;
$status = null;
do {
sleep(5);
try {
$data = $this->request('/cdn/validations/' . $validationTaskId);
} catch (Exception $e) {
throw new Exception('获取验证任务状态失败:' . $e->getMessage());
}
$status = $data['status'];
if ($status === 'failed') {
throw new Exception('证书绑定失败,加速项目验证失败');
}
if ($status === 'succeeded') {
break; // 验证成功立即退出循环
}
$attempts++;
} while ($attempts < $maxAttempts);
if ($status !== 'succeeded') {
throw new Exception('证书绑定超时,加速项目验证时间过长');
}
$this->log('加速项目验证成功,开始部署...');
$deploymentTasks = [
'target' => 'production',
'actions' => [
[
'action' => 'deploy_cert',
'certificateId' => $cert_id,
'version' => 1,
],
[
'action' => 'deploy_property',
'propertyId' => $hostnameInfo["propertyInProduction"]["propertyId"],
'version' => intval($newVersion),
]
],
'name' => 'Deploy certificate and property for ' . $hostnameInfo["propertyInProduction"]["propertyId"],
];
try {
$data = $this->request('/cdn/deploymentTasks', $deploymentTasks, true, null, 'POST', false, ['Check-Certificate' => 'no', 'Check-Usage' => 'no']);
} catch (Exception $e) {
throw new Exception('下发证书部署任务失败:' . $e->getMessage());
}
$url_parts = parse_url($data);
$path_parts = explode('/', $url_parts['path']);
$deploymentTaskId = end($path_parts);
$this->log('CDN域名 ' . $domain . ' 绑定证书部署任务下发成功部署任务ID' . $deploymentTaskId);
$info['cert_id'] = $cert_id;
}
private function get_cert_id($fullchain, $privatekey, $cert_name, $cert_id = null, $serial_no = null, $overwrite = false)
{
if ($cert_id) {
try {
$data = $this->request('/api/certificate/' . $cert_id);
} catch (Exception $e) {
throw new Exception('获取证书详情失败:' . $e->getMessage());
}
if (isset($data['message']) && $data['message'] == 'success' && $data['data']['name'] == $cert_name && $data['data']['serial'] == $serial_no) {
$this->log('证书已是最新证书ID' . $cert_id);
return $cert_id;
}
$this->log('证书已过期或被删除,准备重新上传');
} elseif ($overwrite === true) {
throw new Exception('证书ID不能为空');
}
if ($overwrite === true) {
$param = [
'name' => $cert_name,
'certificate' => $fullchain,
'privateKey' => $privatekey,
];
try {
$data = $this->request('/api/certificate/' . $cert_id, $param, true, null, 'PUT');
$this->log('更新证书成功证书ID' . $cert_id);
return $cert_id;
} catch (Exception $e) {
throw new Exception('更新证书失败:' . $e->getMessage());
}
}
try {
$data = $this->request('/api/ssl/certificate');
} catch (Exception $e) {
throw new Exception('获取证书列表失败:' . $e->getMessage());
}
$certificates = $data['ssl-certificate'];
if (!empty($certificates)) {
foreach ($certificates as $cert) {
if ($serial_no == $cert['certificate-serial']) {
$cert_id = $cert['certificate-id'];
$this->log('证书' . $cert_name . '已存在新证书ID' . $cert_id);
try {
$this->request('/api/certificate/' . $cert_id, ['name' => $cert_name], true, null, 'PUT');
} catch (Exception $e) {
throw new Exception('证书更名失败:' . $e->getMessage());
}
$this->log('将证书ID为' . $cert_id . '的证书更名为:' . $cert_name);
return $cert_id;
} elseif ($cert_name == $cert['name']) {
$this->log('证书' . $cert_name . '已存在,但序列号(' . $cert['certificate-id'] . ')不匹配,准备重新上传');
try {
$this->request('/api/certificate/' . $cert['certificate-id'], ['name' => $cert_name . '-bak'], true, null, 'PUT');
} catch (Exception $e) {
throw new Exception('证书更名失败:' . $e->getMessage());
}
$this->log('将证书ID为' . $cert['certificate-id'] . '的证书更名为:' . $cert_name . '-bak');
}
}
}
$param = [
'name' => $cert_name,
'certificate' => $fullchain,
'privateKey' => $privatekey,
];
try {
$data = $this->request('/api/certificate', $param, true, null, 'POST', true);
} catch (Exception $e) {
throw new Exception('上传证书失败:' . $e->getMessage());
}
$url_parts = parse_url($data);
$path_parts = explode('/', $url_parts['path']);
$cert_id = end($path_parts);
$this->log('上传证书成功证书ID' . $cert_id);
return $cert_id;
}
private function get_cert_id_cdnpro($fullchain, $privatekey, $cert_name)
{
$cert_id = null;
try {
$data = $this->request('/cdn/certificates?search=' . urlencode($cert_name));
} catch (Exception $e) {
throw new Exception('获取证书列表失败:' . $e->getMessage());
}
if ($data['count'] > 0) {
foreach ($data['certificates'] as $cert) {
if ($cert_name == $cert['name']) {
$cert_id = $cert['certificateId'];
$this->log('证书' . $cert_name . '已存在证书ID' . $cert_id);
return $cert_id;
}
}
}
$date = gmdate("D, d M Y H:i:s T");
$encryptedKey = $this->encryptPrivateKey($privatekey, $date);
$param = [
'name' => $cert_name,
'autoRenew' => 'Off',
'newVersion' => [
'privateKey' => $encryptedKey,
'certificate' => $fullchain,
]
];
try {
$data = $this->request('/cdn/certificates', $param, true, $date);
} catch (Exception $e) {
throw new Exception('上传证书失败:' . $e->getMessage());
}
$url_parts = parse_url($data);
$path_parts = explode('/', $url_parts['path']);
$cert_id = end($path_parts);
$this->log('上传证书成功证书ID' . $cert_id);
usleep(500000);
return $cert_id;
}
private function encryptPrivateKey($privateKey, $date = null)
{
// 获取当前 GMT 时间DATE
if (empty($date)) {
$date = gmdate("D, d M Y H:i:s T");
}
// 生成 HMAC-SHA256 密钥材料
if (!empty($this->spKey)) {
$apiKey = $this->spKey;
} else {
$apiKey = $this->apiKey;
}
$hmac = hash_hmac('sha256', $date, $apiKey, true);
$aesIvKeyHex = bin2hex($hmac);
if (strlen($aesIvKeyHex) != 64) {
throw new Exception("Invalid HMAC length: " . strlen($aesIvKeyHex));
}
// 提取 IV 和 Key
$ivHex = substr($aesIvKeyHex, 0, 32);
$keyHex = substr($aesIvKeyHex, 32, 64);
$iv = hex2bin($ivHex);
$key = hex2bin($keyHex);
$blockSize = 16; // AES 块大小为 16 字节
$plainLen = strlen($privateKey);
$padLen = $blockSize - ($plainLen % $blockSize);
$padding = str_repeat(chr($padLen), $padLen);
$plainText = $privateKey . $padding;
// AES-128-CBC 加密
$encrypted = openssl_encrypt(
$plainText,
'AES-128-CBC',
$key,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
$iv
);
if ($encrypted === false) {
throw new Exception("Encryption failed: " . openssl_error_string());
}
// 返回 Base64 编码结果
return base64_encode($encrypted);
}
private function request($path, $data = null, $json = false, $date = null, $method = null, $getLocation = false, $headers = [])
{
$body = null;
if ($data) {
$body = $json ? json_encode($data) : http_build_query($data);
}
if (empty($date)) {
$date = gmdate("D, d M Y H:i:s T");
}
$hmac = hash_hmac('sha1', $date, $this->apiKey, true);
$signature = base64_encode($hmac);
$authorization = 'Basic ' . base64_encode($this->username . ':' . $signature);
if (empty($headers)) {
$headers = [
'Authorization' => $authorization,
'Date' => $date,
'Accept' => 'application/json',
'Connection' => 'close',
];
} else {
$headers['Authorization'] = $authorization;
$headers['Date'] = $date;
$headers['Accept'] = 'application/json';
$headers['Connection'] = 'close';
}
if ($body && $json) {
$headers['Content-Type'] = 'application/json';
}
$url = 'https://open.chinanetcenter.com' . $path;
$response = http_request($url, $body, null, null, $headers, $this->proxy, $method, 30);
$result = json_decode($response['body'], true);
if ((isset($response['code']) && $response['code'] == 201) || (isset($response['code']) && $response['code'] == 200 && $getLocation === true)) {
if (isset($response['headers']['Location'])) {
$location = trim(array_shift($response['headers']['Location'])); // 提取 Location 头部的值并去除多余空格
if (!empty($location)) {
return $location;
}
}
// 如果没有找到 Location 头部,返回默认值 true
return true;
} elseif (isset($response['code']) && $response['code'] >= 200 && $response['code'] <= 299) {
return isset($result) ? $result : true;
} elseif (isset($result['message'])) {
throw new Exception($result['message']);
} elseif (isset($result['result'])) {
throw new Exception($result['result']);
} else {
throw new Exception('请求失败');
}
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
}

131
app/lib/deploy/west.php Normal file
View File

@@ -0,0 +1,131 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class west implements DeployInterface
{
private $logger;
private $username;
private $api_password;
private $baseUrl = 'https://api.west.cn/api/v2';
private $proxy;
public function __construct($config)
{
$this->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);
}
}
}

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

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

View File

@@ -1,314 +1,337 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
use app\lib\client\Aliyun as AliyunClient;
use Exception;
class aliyun implements DnsInterface {
private $AccessKeyId;
private $AccessKeySecret;
private $Endpoint = 'alidns.aliyuncs.com'; //API接入域名
private $Version = '2015-01-09'; //API版本号
private $error;
private $domain;
private $domainid;
private $domainInfo;
class aliyun implements DnsInterface
{
private $AccessKeyId;
private $AccessKeySecret;
private $Endpoint = 'alidns.aliyuncs.com'; //API接入域名
private $Version = '2015-01-09'; //API版本号
private $error;
private $domain;
private $domainid;
private $domainInfo;
private AliyunClient $client;
function __construct($config){
$this->AccessKeyId = $config['ak'];
$this->AccessKeySecret = $config['sk'];
$this->domain = $config['domain'];
}
public function __construct($config)
{
$this->AccessKeyId = $config['ak'];
$this->AccessKeySecret = $config['sk'];
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $this->Endpoint, $this->Version, $proxy);
$this->domain = $config['domain'];
}
public function getError(){
return $this->error;
}
public function getError()
{
return $this->error;
}
public function check(){
if($this->getDomainList() != false){
return true;
}
return false;
}
public function check()
{
if ($this->getDomainList() != false) {
return true;
}
return false;
}
//获取域名列表
public function getDomainList($KeyWord=null, $PageNumber=1, $PageSize=20){
$param = ['Action' => 'DescribeDomains', 'KeyWord' => $KeyWord, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize];
$data = $this->request($param, true);
if($data){
$list = [];
foreach($data['Domains']['Domain'] as $row){
$list[] = [
'DomainId' => $row['DomainId'],
'Domain' => $row['DomainName'],
'RecordCount' => $row['RecordCount'],
];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取域名列表
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20)
{
$param = ['Action' => 'DescribeDomains', 'KeyWord' => $KeyWord, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize];
$data = $this->request($param, true);
if ($data) {
$list = [];
foreach ($data['Domains']['Domain'] as $row) {
$list[] = [
'DomainId' => $row['DomainId'],
'Domain' => $row['DomainName'],
'RecordCount' => $row['RecordCount'],
];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取解析记录列表
public function getDomainRecords($PageNumber=1, $PageSize=20, $KeyWord = null, $SubDomain = null, $Type = null, $Line = null, $Status = null){
$param = ['Action' => 'DescribeDomainRecords', 'DomainName' => $this->domain, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize];
if(!empty($SubDomain) || !empty($Type) || !empty($Line)){
$param += ['SearchMode' => 'ADVANCED', 'RRKeyWord' => $SubDomain, 'ValueKeyWord' => $KeyWord, 'Type' => $Type, 'Line' => $Line, 'ValueKeyWord' => $KeyWord];
}elseif(!empty($KeyWord)){
$param += ['KeyWord' => $KeyWord];
}
if(!isNullOrEmpty($Status)){
$Status = $Status == '1' ? 'Enable' : 'Disable';
$param += ['Status' => $Status];
}
$data = $this->request($param, true);
if($data){
$list = [];
foreach($data['DomainRecords']['Record'] as $row){
$list[] = [
'RecordId' => $row['RecordId'],
'Domain' => $row['DomainName'],
'Name' => $row['RR'],
'Type' => $row['Type'],
'Value' => $row['Value'],
'Line' => $row['Line'],
'TTL' => $row['TTL'],
'MX' => isset($row['Priority']) ? $row['Priority'] : null,
'Status' => $row['Status'] == 'ENABLE' ? '1' : '0',
'Weight' => isset($row['Weight']) ? $row['Weight'] : null,
'Remark' => isset($row['Remark']) ? $row['Remark'] : null,
'UpdateTime' => isset($row['UpdateTimestamp']) ? date('Y-m-d H:i:s', $row['UpdateTimestamp']/1000) : null,
];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取解析记录列表
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$param = ['Action' => 'DescribeDomainRecords', 'DomainName' => $this->domain, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize];
if (!empty($SubDomain) || !empty($Type) || !empty($Line) || !empty($Value)) {
$param += ['SearchMode' => 'ADVANCED', 'RRKeyWord' => $SubDomain, 'ValueKeyWord' => $Value, 'Type' => $Type, 'Line' => $Line];
} elseif (!empty($KeyWord)) {
$param += ['KeyWord' => $KeyWord];
}
if (!isNullOrEmpty($Status)) {
$Status = $Status == '1' ? 'Enable' : 'Disable';
$param += ['Status' => $Status];
}
$data = $this->request($param, true);
if ($data) {
$list = [];
foreach ($data['DomainRecords']['Record'] as $row) {
$list[] = [
'RecordId' => $row['RecordId'],
'Domain' => $row['DomainName'],
'Name' => $row['RR'],
'Type' => $row['Type'],
'Value' => $row['Value'],
'Line' => $row['Line'],
'TTL' => $row['TTL'],
'MX' => isset($row['Priority']) ? $row['Priority'] : null,
'Status' => $row['Status'] == 'ENABLE' ? '1' : '0',
'Weight' => isset($row['Weight']) ? $row['Weight'] : null,
'Remark' => isset($row['Remark']) ? $row['Remark'] : null,
'UpdateTime' => isset($row['UpdateTimestamp']) ? date('Y-m-d H:i:s', intval($row['UpdateTimestamp'] / 1000)) : null,
];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){
$param = ['Action' => 'DescribeSubDomainRecords', 'SubDomain' => $SubDomain . '.' . $this->domain, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'Type' => $Type, 'Line' => $Line];
$data = $this->request($param, true);
if($data){
$list = [];
foreach($data['DomainRecords']['Record'] as $row){
$list[] = [
'RecordId' => $row['RecordId'],
'Domain' => $row['DomainName'],
'Name' => $row['RR'],
'Type' => $row['Type'],
'Value' => $row['Value'],
'Line' => $row['Line'],
'TTL' => $row['TTL'],
'MX' => isset($row['Priority']) ? $row['Priority'] : null,
'Status' => $row['Status'] == 'ENABLE' ? '1' : '0',
'Weight' => isset($row['Weight']) ? $row['Weight'] : null,
'Remark' => isset($row['Remark']) ? $row['Remark'] : null,
'UpdateTime' => isset($row['UpdateTimestamp']) ? date('Y-m-d H:i:s', $row['UpdateTimestamp']/1000) : null,
];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取子域名解析记录列表
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
{
$param = ['Action' => 'DescribeSubDomainRecords', 'SubDomain' => $SubDomain . '.' . $this->domain, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'Type' => $Type, 'Line' => $Line];
$data = $this->request($param, true);
if ($data) {
$list = [];
foreach ($data['DomainRecords']['Record'] as $row) {
$list[] = [
'RecordId' => $row['RecordId'],
'Domain' => $row['DomainName'],
'Name' => $row['RR'],
'Type' => $row['Type'],
'Value' => $row['Value'],
'Line' => $row['Line'],
'TTL' => $row['TTL'],
'MX' => isset($row['Priority']) ? $row['Priority'] : null,
'Status' => $row['Status'] == 'ENABLE' ? '1' : '0',
'Weight' => isset($row['Weight']) ? $row['Weight'] : null,
'Remark' => isset($row['Remark']) ? $row['Remark'] : null,
'UpdateTime' => isset($row['UpdateTimestamp']) ? date('Y-m-d H:i:s', intval($row['UpdateTimestamp'] / 1000)) : null,
];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId){
$param = ['Action' => 'DescribeDomainRecordInfo', 'RecordId' => $RecordId];
$data = $this->request($param, true);
if($data){
return [
'RecordId' => $data['RecordId'],
'Domain' => $data['DomainName'],
'Name' => $data['RR'],
'Type' => $data['Type'],
'Value' => $data['Value'],
'Line' => $data['Line'],
'TTL' => $data['TTL'],
'MX' => isset($data['Priority']) ? $data['Priority'] : null,
'Status' => $data['Status'] == 'ENABLE' ? '1' : '0',
'Weight' => isset($data['Weight']) ? $data['Weight'] : null,
'Remark' => isset($data['Remark']) ? $data['Remark'] : null,
'UpdateTime' => isset($row['UpdateTimestamp']) ? date('Y-m-d H:i:s', $data['UpdateTimestamp']/1000) : null,
];
}
return false;
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId)
{
$param = ['Action' => 'DescribeDomainRecordInfo', 'RecordId' => $RecordId];
$data = $this->request($param, true);
if ($data) {
return [
'RecordId' => $data['RecordId'],
'Domain' => $data['DomainName'],
'Name' => $data['RR'],
'Type' => $data['Type'],
'Value' => $data['Value'],
'Line' => $data['Line'],
'TTL' => $data['TTL'],
'MX' => isset($data['Priority']) ? $data['Priority'] : null,
'Status' => $data['Status'] == 'ENABLE' ? '1' : '0',
'Weight' => isset($data['Weight']) ? $data['Weight'] : null,
'Remark' => isset($data['Remark']) ? $data['Remark'] : null,
'UpdateTime' => isset($row['UpdateTimestamp']) ? date('Y-m-d H:i:s', intval($data['UpdateTimestamp'] / 1000)) : null,
];
}
return false;
}
//添加解析记录
public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = null, $Remark = null){
$param = ['Action' => 'AddDomainRecord', 'DomainName' => $this->domain, 'RR' => $Name, 'Type' => $Type, 'Value' => $Value, 'Line' => $this->convertLineCode($Line), 'TTL' => intval($TTL)];
if($MX){
$params['Priority'] = intval($MX);
}
$data = $this->request($param, true);
if($data){
return $data['RecordId'];
}
return false;
}
//添加解析记录
public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = null, $Weight = null, $Remark = null)
{
$param = ['Action' => 'AddDomainRecord', 'DomainName' => $this->domain, 'RR' => $Name, 'Type' => $Type, 'Value' => $Value, 'Line' => $this->convertLineCode($Line), 'TTL' => intval($TTL)];
if ($MX) {
$param['Priority'] = intval($MX);
}
$data = $this->request($param, true);
if ($data) {
return $data['RecordId'];
}
return false;
}
//修改解析记录
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = null, $Remark = null){
$param = ['Action' => 'UpdateDomainRecord', 'RecordId' => $RecordId, 'RR' => $Name, 'Type' => $Type, 'Value' => $Value, 'Line' => $this->convertLineCode($Line), 'TTL' => intval($TTL)];
if($MX){
$params['Priority'] = intval($MX);
}
return $this->request($param);
}
//修改解析记录
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = null, $Weight = null, $Remark = null)
{
$param = ['Action' => 'UpdateDomainRecord', 'RecordId' => $RecordId, 'RR' => $Name, 'Type' => $Type, 'Value' => $Value, 'Line' => $this->convertLineCode($Line), 'TTL' => intval($TTL)];
if ($MX) {
$param['Priority'] = intval($MX);
}
return $this->request($param);
}
//修改解析记录备注
public function updateDomainRecordRemark($RecordId, $Remark){
$param = ['Action' => 'UpdateDomainRecordRemark', 'RecordId' => $RecordId, 'Remark' => $Remark];
return $this->request($param);
}
//修改解析记录备注
public function updateDomainRecordRemark($RecordId, $Remark)
{
$param = ['Action' => 'UpdateDomainRecordRemark', 'RecordId' => $RecordId, 'Remark' => $Remark];
return $this->request($param);
}
//删除解析记录
public function deleteDomainRecord($RecordId){
$param = ['Action' => 'DeleteDomainRecord', 'RecordId' => $RecordId];
return $this->request($param);
}
//删除解析记录
public function deleteDomainRecord($RecordId)
{
$param = ['Action' => 'DeleteDomainRecord', 'RecordId' => $RecordId];
return $this->request($param);
}
//删除子域名的解析记录
public function deleteSubDomainRecords($SubDomain){
$param = ['Action' => 'DeleteSubDomainRecords', 'DomainName' => $this->domain, 'RR' => $SubDomain];
return $this->request($param);
}
//删除子域名的解析记录
public function deleteSubDomainRecords($SubDomain)
{
$param = ['Action' => 'DeleteSubDomainRecords', 'DomainName' => $this->domain, 'RR' => $SubDomain];
return $this->request($param);
}
//设置解析记录状态
public function setDomainRecordStatus($RecordId, $Status){
$Status = $Status == '1' ? 'Enable' : 'Disable';
$param = ['Action' => 'SetDomainRecordStatus', 'RecordId' => $RecordId, 'Status' => $Status];
return $this->request($param);
}
//设置解析记录状态
public function setDomainRecordStatus($RecordId, $Status)
{
$Status = $Status == '1' ? 'Enable' : 'Disable';
$param = ['Action' => 'SetDomainRecordStatus', 'RecordId' => $RecordId, 'Status' => $Status];
return $this->request($param);
}
//获取解析记录操作日志
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null){
$param = ['Action' => 'DescribeRecordLogs', 'DomainName' => $this->domain, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'KeyWord' => $KeyWord, 'StartDate' => $StartDate, 'endDate' => $endDate, 'Lang' => 'zh'];
$data = $this->request($param, true);
if($data){
$list = [];
foreach($data['RecordLogs']['RecordLog'] as $row){
$list[] = ['time'=>date('Y-m-d H:i:s', intval($row['ActionTimestamp']/1000)), 'data'=>$row['Message']];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取解析记录操作日志
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
{
$param = ['Action' => 'DescribeRecordLogs', 'DomainName' => $this->domain, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'KeyWord' => $KeyWord, 'StartDate' => $StartDate, 'endDate' => $endDate, 'Lang' => 'zh'];
$data = $this->request($param, true);
if ($data) {
$list = [];
foreach ($data['RecordLogs']['RecordLog'] as $row) {
$list[] = ['time' => date('Y-m-d H:i:s', intval($row['ActionTimestamp'] / 1000)), 'data' => $row['Message']];
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//获取解析线路列表
public function getRecordLine(){
$data = $this->getDomainInfo();
if($data){
$list = [];
foreach($data['RecordLines']['RecordLine'] as $row){
$list[$row['LineCode']] = ['name'=>$row['LineDisplayName'], 'parent'=>isset($row['FatherCode']) ? $row['FatherCode'] : null];
}
return $list;
}
return false;
}
//获取解析线路列表
public function getRecordLine()
{
$data = $this->getDomainInfo();
if ($data) {
$list = [];
foreach ($data['RecordLines']['RecordLine'] as $row) {
$list[$row['LineCode']] = ['name' => $row['LineDisplayName'], 'parent' => isset($row['FatherCode']) ? $row['FatherCode'] : null];
}
return $list;
}
return false;
}
//获取域名信息
public function getDomainInfo(){
if(!empty($this->domainInfo)) return $this->domainInfo;
$param = ['Action' => 'DescribeDomainInfo', 'DomainName' => $this->domain, 'NeedDetailAttributes' => 'true'];
$data = $this->request($param, true);
if($data){
$this->domainInfo = $data;
return $data;
}
return false;
}
//获取域名信息
public function getDomainInfo()
{
if (!empty($this->domainInfo)) return $this->domainInfo;
$param = ['Action' => 'DescribeDomainInfo', 'DomainName' => $this->domain, 'NeedDetailAttributes' => 'true'];
$data = $this->request($param, true);
if ($data) {
$this->domainInfo = $data;
return $data;
}
return false;
}
//获取域名最低TTL
public function getMinTTL(){
$data = $this->getDomainInfo();
if($data){
return $data['MinTtl'];
}
return false;
}
//获取域名最低TTL
public function getMinTTL()
{
$data = $this->getDomainInfo();
if ($data) {
return $data['MinTtl'];
}
return false;
}
private function convertLineCode($line){
$convert_dict = ['0'=>'default', '10=1'=>'unicom', '10=0'=>'telecom', '10=3'=>'mobile', '10=2'=>'edu', '3=0'=>'oversea', '10=22'=>'btvn', '80=0'=>'search', '7=0'=>'internal'];
if(array_key_exists($line, $convert_dict)){
return $convert_dict[$line];
}
return $line;
}
//获取权重配置子域名列表
public function getWeightSubDomains($PageNumber = 1, $PageSize = 20, $SubDomain = null)
{
$param = ['Action' => 'DescribeDNSSLBSubDomains', 'DomainName' => $this->domain, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize];
if (!empty($SubDomain)) {
$param += ['Rr' => $SubDomain];
}
$data = $this->request($param, true);
if ($data) {
$list = $data['SlbSubDomains']['SlbSubDomain'];
$i = 1;
foreach ($list as &$v) {
$v['id'] = $i++;
$v['rr'] = substr($v['SubDomain'], 0, -strlen($this->domain) - 1);
}
return ['total' => $data['TotalCount'], 'list' => $list];
}
return false;
}
//开启关闭权重配置
public function setWeightStatus($SubDomain, $Open, $Type = null, $Line = null)
{
$param = ['Action' => 'SetDNSSLBStatus', 'DomainName' => $this->domain, 'SubDomain' => $SubDomain, 'Open' => $Open == '1' ? 'true' : 'false'];
if (!empty($Type)) {
$param += ['Type' => $Type];
}
if (!empty($Line)) {
$param += ['Line' => $Line];
}
return $this->request($param);
}
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));
//修改权重
public function updateRecordWeight($RecordId, $Weight)
{
$param = ['Action' => 'UpdateDNSSLBWeight', 'RecordId' => $RecordId, 'Weight' => $Weight];
return $this->request($param);
}
return $signature;
}
private function percentEncode($str)
{
$search = ['+', '*', '%7E'];
$replace = ['%20', '%2A', '~'];
return str_replace($search, $replace, urlencode($str));
}
private function request($param, $returnData=false){
if(empty($this->AccessKeyId)||empty($this->AccessKeySecret))return false;
$result = $this->request_do($param, $returnData);
if(!$returnData && $result!==true){
usleep(50000);
$result = $this->request_do($param, $returnData);
}
return $result;
}
private function request_do($param, $returnData=false){
if(empty($this->AccessKeyId)||empty($this->AccessKeySecret))return false;
$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, 'POST');
$ch = curl_init($url);
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);
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) {
$this->setError('Curl error: ' . curl_error($ch));
}
curl_close($ch);
if ($errno) return false;
public function addDomain($Domain)
{
$param = ['Action' => 'AddDomain', 'DomainName' => $Domain];
$result = $this->request($param, true);
if ($result) {
return ['id' => $result['DomainId'], 'name' => $result['DomainName']];
}
return false;
}
$arr = json_decode($response,true);
if($httpCode==200){
return $returnData ? $arr : true;
}elseif($arr){
$this->setError($arr['Message']);
return false;
}else{
$this->setError('返回数据解析失败');
return false;
}
}
private function convertLineCode($line)
{
$convert_dict = ['0' => 'default', '10=1' => 'unicom', '10=0' => 'telecom', '10=3' => 'mobile', '10=2' => 'edu', '3=0' => 'oversea', '10=22' => 'btvn', '80=0' => 'search', '7=0' => 'internal'];
if (array_key_exists($line, $convert_dict)) {
return $convert_dict[$line];
}
return $line;
}
private function setError($message){
$this->error = $message;
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
}
private function request($param, $returnData = false)
{
if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) return false;
try {
$result = $this->client->request($param);
} catch (Exception $e) {
try {
usleep(50000);
$result = $this->client->request($param);
} catch (Exception $e) {
$this->setError($e->getMessage());
return false;
}
}
return $returnData ? $result : true;
}
private function setError($message)
{
$this->error = $message;
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
}
}

View File

@@ -1,325 +1,258 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
class baidu implements DnsInterface {
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint = "dns.baidubce.com";
private $error;
private $domain;
private $domainid;
function __construct($config){
$this->AccessKeyId = $config['ak'];
$this->SecretAccessKey = $config['sk'];
$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, $Type = null, $Line = null, $Status = null){
$marker = cookie('baidu_record_marker');
$query = ['rr' => $KeyWord];
if(!isNullOrEmpty(($SubDomain))){
$param['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,
];
}
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, $Type, $Line);
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId){
$data = $this->send_reuqest('GET', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId);
if($data){
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' => $data['weight'],
'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, $Remark = null){
$params = ['rr' => $Name, 'type' => $this->convertType($Type), 'value' => $Value, 'line'=>$Line, 'ttl' => intval($TTL), 'description' => $Remark];
if($Type == 'MX')$param['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, $Remark = null){
$params = ['rr' => $Name, 'type' => $this->convertType($Type), 'value' => $Value, 'line'=>$Line, 'ttl' => intval($TTL), 'description' => $Remark];
if($Type == 'MX')$param['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;
}
private function convertType($type){
return $type;
}
private function send_reuqest($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);
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) {
$this->setError('Curl error: ' . curl_error($ch));
}
curl_close($ch);
if ($errno) return false;
if(empty($response) && $httpCode == 200){
return true;
}
$arr=json_decode($response,true);
if($arr){
if(isset($arr['code']) && isset($arr['message'])){
$this->setError($arr['message']);
return false;
}else{
return $arr;
}
}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);
}
}
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
use app\lib\client\BaiduCloud;
use Exception;
class baidu implements DnsInterface
{
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint = "dns.baidubce.com";
private $error;
private $domain;
private $domainid;
private BaiduCloud $client;
public function __construct($config)
{
$this->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);
}
}

View File

@@ -1,237 +1,335 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
class cloudflare implements DnsInterface {
private $Email;
private $ApiKey;
private $baseUrl = 'https://api.cloudflare.com/client/v4';
private $error;
private $domain;
private $domainid;
function __construct($config){
$this->Email = $config['ak'];
$this->ApiKey = $config['sk'];
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
}
public function getError(){
return $this->error;
}
public function check(){
if($this->getDomainList() != false){
return true;
}
return false;
}
//获取域名列表
public function getDomainList($KeyWord=null, $PageNumber=1, $PageSize=20){
$param = ['page' => $PageNumber, 'per_page' => $PageSize, '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, $Type = null, $Line = null, $Status = null){
$param = ['name' => $SubDomain, 'type' => $Type, 'search' => $KeyWord, 'page' => $PageNumber, 'per_page' => $PageSize];
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 = $row['zone_name'] == $row['name'] ? '@' : str_replace('.'.$row['zone_name'], '', $row['name']);
$list[] = [
'RecordId' => $row['id'],
'Domain' => $row['zone_name'],
'Name' => $name,
'Type' => $row['type'],
'Value' => $row['content'],
'Line' => $row['proxied'] ? '1' : '0',
'TTL' => $row['ttl'],
'MX' => isset($row['priority']) ? $row['priority'] : null,
'Status' => $row['locked'] ? '0' : '1',
'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){
if($SubDomain == '@')$SubDomain=$this->domain;
else $SubDomain .= '.'.$this->domain;
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line);
}
//获取解析记录详细信息
public function getDomainRecordInfo($RecordId){
$data = $this->send_reuqest('GET', '/zones/'.$this->domainid.'/dns_records/'.$RecordId);
if($data){
$name = $data['result']['zone_name'] == $data['result']['name'] ? '@' : str_replace('.'.$data['result']['zone_name'], '', $data['result']['name']);
return [
'RecordId' => $data['result']['id'],
'Domain' => $data['result']['zone_name'],
'Name' => str_replace('.'.$data['result']['zone_name'], '', $data['result']['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' => $data['result']['locked'] ? '0' : '1',
'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, $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);
$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, $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);
$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){
return false;
}
//获取解析记录操作日志
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null){
return false;
}
//获取解析线路列表
public function getRecordLine(){
return ['0'=>['name'=>'仅DNS', 'parent'=>null], '1'=>['name'=>'已代理', 'parent'=>null]];
}
//获取域名信息
public function getDomainInfo(){
$data = $this->send_reuqest('GET', '/zones/'.$this->domainid);
if($data){
return $data['result'];
}
return false;
}
//获取域名最低TTL
public function getMinTTL(){
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 send_reuqest($method, $path, $params = null){
$url = $this->baseUrl . $path;
$headers = [
'X-Auth-Email: '.$this->Email,
'X-Auth-Key: '.$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);
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);
}
}
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
class cloudflare implements DnsInterface
{
private $Email;
private $ApiKey;
private $baseUrl = 'https://api.cloudflare.com/client/v4';
private $error;
private $domain;
private $domainid;
private $proxy;
function __construct($config)
{
$this->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;
if ($row['type'] == 'SRV' && isset($row['priority'])) {
$row['content'] = $row['priority'] . ' ' . $row['content'];
}
$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;
if ($data['result']['type'] == 'SRV' && isset($data['result']['priority'])) {
$data['result']['content'] = $data['result']['priority'] . ' ' . $data['result']['content'];
}
return [
'RecordId' => $data['result']['id'],
'Domain' => $this->domain,
'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-f]+$/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);
}
}

Some files were not shown because too many files have changed in this diff Show More