├── app
├── .htaccess
├── Request.php
├── service.php
├── provider.php
├── lib
│ ├── DeployInterface.php
│ ├── NewDb.php
│ ├── acme
│ │ └── ACME_Exception.php
│ ├── CertInterface.php
│ ├── NewDbManager.php
│ ├── DnsInterface.php
│ ├── mail
│ │ ├── Sendcloud.php
│ │ └── Aliyun.php
│ ├── client
│ │ ├── Ucloud.php
│ │ └── Aliyun.php
│ ├── deploy
│ │ ├── cachefly.php
│ │ ├── rainyun.php
│ │ ├── gcore.php
│ │ ├── baishan.php
│ │ ├── local.php
│ │ ├── kuocai.php
│ │ ├── ksyun.php
│ │ ├── proxmox.php
│ │ ├── uusec.php
│ │ ├── xp.php
│ │ ├── safeline.php
│ │ ├── lucky.php
│ │ ├── lecdn.php
│ │ ├── mwpanel.php
│ │ ├── ftp.php
│ │ ├── cdnfly.php
│ │ ├── doge.php
│ │ ├── ucloud.php
│ │ └── west.php
│ └── cert
│ │ ├── letsencrypt.php
│ │ └── customacme.php
├── event.php
├── AppService.php
├── middleware.php
├── middleware
│ ├── CheckLogin.php
│ ├── RefererCheck.php
│ ├── ViewOutput.php
│ ├── AuthApi.php
│ ├── LoadConfig.php
│ └── AuthUser.php
├── command
│ ├── Certtask.php
│ ├── Reset.php
│ └── Dmtask.php
├── view
│ ├── domain
│ │ ├── log.html
│ │ └── expire_notice.html
│ ├── system
│ │ ├── loginset.html
│ │ ├── proxyset.html
│ │ └── cronset.html
│ ├── user
│ │ └── log.html
│ ├── dmonitor
│ │ └── taskinfo.html
│ ├── dispatch_jump.html
│ ├── cert
│ │ ├── certaccount.html
│ │ └── deployaccount.html
│ └── optimizeip
│ │ └── opipset.html
├── ExceptionHandle.php
├── utils
│ └── DnsQueryUtils.php
├── BaseController.php
├── controller
│ └── Install.php
└── service
│ ├── CertTaskService.php
│ └── ExpireNoticeService.php
├── public
├── static
│ ├── css
│ │ └── custom.css
│ ├── images
│ │ ├── aws.ico
│ │ ├── aws.png
│ │ ├── bt.png
│ │ ├── ssl.ico
│ │ ├── waf.png
│ │ ├── xp.png
│ │ ├── baidu.ico
│ │ ├── cloud.png
│ │ ├── ctyun.ico
│ │ ├── dnsla.ico
│ │ ├── doge.png
│ │ ├── fnos.png
│ │ ├── gcore.ico
│ │ ├── host.png
│ │ ├── ksyun.ico
│ │ ├── logo.png
│ │ ├── lucky.png
│ │ ├── qiniu.ico
│ │ ├── upyun.ico
│ │ ├── user.png
│ │ ├── west.ico
│ │ ├── aliyun.ico
│ │ ├── aliyun.png
│ │ ├── dnspod.ico
│ │ ├── google.ico
│ │ ├── huawei.ico
│ │ ├── huoshan.ico
│ │ ├── jdcloud.ico
│ │ ├── kuocai.jpg
│ │ ├── maoyun.png
│ │ ├── mwpanel.ico
│ │ ├── namesilo.ico
│ │ ├── opanel.png
│ │ ├── powerdns.ico
│ │ ├── proxmox.ico
│ │ ├── ratpanel.ico
│ │ ├── safeline.png
│ │ ├── server.png
│ │ ├── server2.png
│ │ ├── synology.png
│ │ ├── tencent.png
│ │ ├── ucloud.ico
│ │ ├── unicloud.png
│ │ ├── wangsu.ico
│ │ ├── zerossl.ico
│ │ ├── cloudflare.ico
│ │ ├── login-head.png
│ │ ├── spaceship.ico
│ │ ├── letsencrypt.ico
│ │ ├── success.svg
│ │ └── error.svg
│ ├── fonts
│ │ ├── fontawesome-webfont.eot
│ │ ├── fontawesome-webfont.ttf
│ │ ├── fontawesome-webfont.woff
│ │ ├── fontawesome-webfont.woff2
│ │ ├── glyphicons-halflings-regular.eot
│ │ ├── glyphicons-halflings-regular.ttf
│ │ ├── glyphicons-halflings-regular.woff
│ │ └── glyphicons-halflings-regular.woff2
│ └── js
│ │ ├── layer
│ │ ├── theme
│ │ │ └── default
│ │ │ │ ├── icon.png
│ │ │ │ ├── icon-ext.png
│ │ │ │ ├── loading-0.gif
│ │ │ │ ├── loading-1.gif
│ │ │ │ └── loading-2.gif
│ │ └── mobile
│ │ │ └── layer.js
│ │ ├── select2-i18n-zh-CN-4.0.13.min.js
│ │ ├── html5shiv-3.7.3.min.js
│ │ ├── FileSaver-2.0.5.min.js
│ │ ├── respond-1.4.2.min.js
│ │ └── moment-2.29.4-locale-zh-cn.js
├── robots.txt
├── favicon.ico
├── .htaccess
├── router.php
└── index.php
├── .gitattributes
├── runtime
└── .gitignore
├── .gitignore
├── config
├── middleware.php
├── trace.php
├── console.php
├── session.php
├── cookie.php
├── filesystem.php
├── view.php
├── lang.php
├── cache.php
├── captcha.php
├── app.php
├── log.php
├── route.php
└── database.php
├── think
├── .example.env
├── .github
└── dependabot.yml
├── LICENSE
└── composer.json
/app/.htaccess:
--------------------------------------------------------------------------------
1 | deny from all
--------------------------------------------------------------------------------
/public/static/css/custom.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/runtime/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /.vscode
3 | /vendor
4 | *.log
5 | .env
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/static/images/aws.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/aws.ico
--------------------------------------------------------------------------------
/public/static/images/aws.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/aws.png
--------------------------------------------------------------------------------
/public/static/images/bt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/bt.png
--------------------------------------------------------------------------------
/public/static/images/ssl.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/ssl.ico
--------------------------------------------------------------------------------
/public/static/images/waf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/waf.png
--------------------------------------------------------------------------------
/public/static/images/xp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/xp.png
--------------------------------------------------------------------------------
/public/static/images/baidu.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/baidu.ico
--------------------------------------------------------------------------------
/public/static/images/cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/cloud.png
--------------------------------------------------------------------------------
/public/static/images/ctyun.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/ctyun.ico
--------------------------------------------------------------------------------
/public/static/images/dnsla.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/dnsla.ico
--------------------------------------------------------------------------------
/public/static/images/doge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/doge.png
--------------------------------------------------------------------------------
/public/static/images/fnos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/fnos.png
--------------------------------------------------------------------------------
/public/static/images/gcore.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/gcore.ico
--------------------------------------------------------------------------------
/public/static/images/host.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/host.png
--------------------------------------------------------------------------------
/public/static/images/ksyun.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/ksyun.ico
--------------------------------------------------------------------------------
/public/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/logo.png
--------------------------------------------------------------------------------
/public/static/images/lucky.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/lucky.png
--------------------------------------------------------------------------------
/public/static/images/qiniu.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/qiniu.ico
--------------------------------------------------------------------------------
/public/static/images/upyun.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/upyun.ico
--------------------------------------------------------------------------------
/public/static/images/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/user.png
--------------------------------------------------------------------------------
/public/static/images/west.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/west.ico
--------------------------------------------------------------------------------
/public/static/images/aliyun.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/aliyun.ico
--------------------------------------------------------------------------------
/public/static/images/aliyun.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/aliyun.png
--------------------------------------------------------------------------------
/public/static/images/dnspod.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/dnspod.ico
--------------------------------------------------------------------------------
/public/static/images/google.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/google.ico
--------------------------------------------------------------------------------
/public/static/images/huawei.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/huawei.ico
--------------------------------------------------------------------------------
/public/static/images/huoshan.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/huoshan.ico
--------------------------------------------------------------------------------
/public/static/images/jdcloud.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/jdcloud.ico
--------------------------------------------------------------------------------
/public/static/images/kuocai.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/kuocai.jpg
--------------------------------------------------------------------------------
/public/static/images/maoyun.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/maoyun.png
--------------------------------------------------------------------------------
/public/static/images/mwpanel.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/mwpanel.ico
--------------------------------------------------------------------------------
/public/static/images/namesilo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/namesilo.ico
--------------------------------------------------------------------------------
/public/static/images/opanel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/opanel.png
--------------------------------------------------------------------------------
/public/static/images/powerdns.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/powerdns.ico
--------------------------------------------------------------------------------
/public/static/images/proxmox.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/proxmox.ico
--------------------------------------------------------------------------------
/public/static/images/ratpanel.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/ratpanel.ico
--------------------------------------------------------------------------------
/public/static/images/safeline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/safeline.png
--------------------------------------------------------------------------------
/public/static/images/server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/server.png
--------------------------------------------------------------------------------
/public/static/images/server2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/server2.png
--------------------------------------------------------------------------------
/public/static/images/synology.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/synology.png
--------------------------------------------------------------------------------
/public/static/images/tencent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/tencent.png
--------------------------------------------------------------------------------
/public/static/images/ucloud.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/ucloud.ico
--------------------------------------------------------------------------------
/public/static/images/unicloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/unicloud.png
--------------------------------------------------------------------------------
/public/static/images/wangsu.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/wangsu.ico
--------------------------------------------------------------------------------
/public/static/images/zerossl.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netcccyun/dnsmgr/HEAD/public/static/images/zerossl.ico
--------------------------------------------------------------------------------
/app/Request.php:
--------------------------------------------------------------------------------
1 | [],
6 | // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
7 | 'priority' => [],
8 | ];
9 |
--------------------------------------------------------------------------------
/app/provider.php:
--------------------------------------------------------------------------------
1 | Request::class,
8 | 'think\exception\Handle' => ExceptionHandle::class,
9 | ];
10 |
--------------------------------------------------------------------------------
/app/lib/DeployInterface.php:
--------------------------------------------------------------------------------
1 |
2 | Options +FollowSymlinks -Multiviews
3 | RewriteEngine On
4 |
5 | RewriteCond %{REQUEST_FILENAME} !-d
6 | RewriteCond %{REQUEST_FILENAME} !-f
7 | RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
8 |
9 |
--------------------------------------------------------------------------------
/think:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | = 8.0 !');
7 | }
8 |
9 | // 命令行入口文件
10 | // 加载基础文件
11 | require __DIR__ . '/vendor/autoload.php';
12 |
13 | // 应用初始化
14 | (new App())->console->run();
--------------------------------------------------------------------------------
/app/event.php:
--------------------------------------------------------------------------------
1 | [
5 | ],
6 |
7 | 'listen' => [
8 | 'AppInit' => [],
9 | 'HttpRun' => [],
10 | 'HttpEnd' => [],
11 | 'LogLevel' => [],
12 | 'LogWrite' => [],
13 | ],
14 |
15 | 'subscribe' => [
16 | ],
17 | ];
18 |
--------------------------------------------------------------------------------
/config/trace.php:
--------------------------------------------------------------------------------
1 | 'Html',
8 | // 读取的日志通道名
9 | 'channel' => '',
10 | ];
11 |
--------------------------------------------------------------------------------
/.example.env:
--------------------------------------------------------------------------------
1 | APP_DEBUG = false
2 |
3 | [APP]
4 | DEFAULT_TIMEZONE = Asia/Shanghai
5 |
6 | [DATABASE]
7 | TYPE = mysql
8 | HOSTNAME = {dbhost}
9 | DATABASE = {dbname}
10 | USERNAME = {dbuser}
11 | PASSWORD = {dbpwd}
12 | HOSTPORT = {dbport}
13 | CHARSET = utf8mb4
14 | PREFIX = {dbprefix}
15 | DEBUG = false
16 |
17 | [LANG]
18 | default_lang = zh-cn
--------------------------------------------------------------------------------
/app/AppService.php:
--------------------------------------------------------------------------------
1 | [
8 | 'dmtask' => 'app\command\Dmtask',
9 | 'certtask' => 'app\command\Certtask',
10 | 'reset' => 'app\command\Reset',
11 | ],
12 | ];
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Dependabot configuration.
2 | #
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/working-with-dependabot/dependabot-options-reference
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "daily"
12 | - package-ecosystem: "composer"
13 | directory: "/"
14 | schedule:
15 | interval: "daily"
16 |
--------------------------------------------------------------------------------
/app/lib/acme/ACME_Exception.php:
--------------------------------------------------------------------------------
1 | type = $type;
13 | $this->subproblems = $subproblems;
14 | parent::__construct($detail);
15 | }
16 | function getType()
17 | {
18 | return $this->type;
19 | }
20 | function getSubproblems()
21 | {
22 | return $this->subproblems;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/middleware/CheckLogin.php:
--------------------------------------------------------------------------------
1 | islogin) {
12 | if ($request->isAjax() || !$request->isGet()) {
13 | return json(['code' => -1, 'msg' => '未登录'])->code(401);
14 | }
15 | return redirect((string)url('/login'));
16 | }
17 | return $next($request);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/middleware/RefererCheck.php:
--------------------------------------------------------------------------------
1 | 'PHPSESSID',
9 | // SESSION_ID的提交变量,解决flash上传跨域
10 | 'var_session_id' => '',
11 | // 驱动方式 支持file cache
12 | 'type' => 'file',
13 | // 存储连接标识 当type使用cache的时候有效
14 | 'store' => null,
15 | // 过期时间
16 | 'expire' => 1440,
17 | // 前缀
18 | 'prefix' => '',
19 | ];
20 |
--------------------------------------------------------------------------------
/app/middleware/ViewOutput.php:
--------------------------------------------------------------------------------
1 | islogin);
21 | View::assign('user', $request->user);
22 | View::assign('skin', getAdminSkin());
23 | return $next($request);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/config/cookie.php:
--------------------------------------------------------------------------------
1 | 0,
8 | // cookie 保存路径
9 | 'path' => '/',
10 | // cookie 有效域名
11 | 'domain' => '',
12 | // cookie 启用安全传输
13 | 'secure' => false,
14 | // httponly设置
15 | 'httponly' => false,
16 | // 是否使用 setcookie
17 | 'setcookie' => true,
18 | // samesite 设置,支持 'strict' 'lax'
19 | 'samesite' => '',
20 | ];
21 |
--------------------------------------------------------------------------------
/public/static/js/select2-i18n-zh-CN-4.0.13.min.js:
--------------------------------------------------------------------------------
1 | !function(){var n;jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd&&(n=jQuery.fn.select2.amd),n.define("select2/i18n/zh-CN",[],function(){return{errorLoading:function(){return"无法载入结果。"},inputTooLong:function(n){return"请删除"+(n.input.length-n.maximum)+"个字符"},inputTooShort:function(n){return"请再输入至少"+(n.minimum-n.input.length)+"个字符"},loadingMore:function(){return"载入更多结果…"},maximumSelected:function(n){return"最多只能选择"+n.maximum+"个项目"},noResults:function(){return"未找到结果"},searching:function(){return"搜索中…"},removeAllItems:function(){return"删除所有项目"}}}),n.define,n.require}();
--------------------------------------------------------------------------------
/config/filesystem.php:
--------------------------------------------------------------------------------
1 | env('filesystem.driver', 'local'),
6 | // 磁盘列表
7 | 'disks' => [
8 | 'local' => [
9 | 'type' => 'local',
10 | 'root' => app()->getRuntimePath() . 'storage',
11 | ],
12 | 'public' => [
13 | // 磁盘类型
14 | 'type' => 'local',
15 | // 磁盘路径
16 | 'root' => app()->getRootPath() . 'public/storage',
17 | // 磁盘路径对应的外部URL路径
18 | 'url' => '/storage',
19 | // 可见性
20 | 'visibility' => 'public',
21 | ],
22 | // 更多的磁盘配置信息
23 | ],
24 | ];
25 |
--------------------------------------------------------------------------------
/app/lib/NewDbManager.php:
--------------------------------------------------------------------------------
1 | getConfig('default', 'mysql');
21 | }
22 |
23 | return $this->createConnection($name);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/config/view.php:
--------------------------------------------------------------------------------
1 | 'Think',
9 | // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
10 | 'auto_rule' => 1,
11 | // 模板目录名
12 | 'view_dir_name' => 'view',
13 | // 模板后缀
14 | 'view_suffix' => 'html',
15 | // 模板文件名分隔符
16 | 'view_depr' => DIRECTORY_SEPARATOR,
17 | // 模板引擎普通标签开始标记
18 | 'tpl_begin' => '{',
19 | // 模板引擎普通标签结束标记
20 | 'tpl_end' => '}',
21 | // 标签库标签开始标记
22 | 'taglib_begin' => '{',
23 | // 标签库标签结束标记
24 | 'taglib_end' => '}',
25 | ];
26 |
--------------------------------------------------------------------------------
/config/lang.php:
--------------------------------------------------------------------------------
1 | env('lang.default_lang', 'zh-cn'),
9 | // 允许的语言列表
10 | 'allow_lang_list' => [],
11 | // 多语言自动侦测变量名
12 | 'detect_var' => 'lang',
13 | // 是否使用Cookie记录
14 | 'use_cookie' => true,
15 | // 多语言cookie变量
16 | 'cookie_var' => 'think_lang',
17 | // 多语言header变量
18 | 'header_var' => 'think-lang',
19 | // 扩展语言包
20 | 'extend_list' => [],
21 | // Accept-Language转义为对应语言包名称
22 | 'accept_language' => [
23 | 'zh-hans-cn' => 'zh-cn',
24 | ],
25 | // 是否支持语言分组
26 | 'allow_group' => false,
27 | ];
28 |
--------------------------------------------------------------------------------
/config/cache.php:
--------------------------------------------------------------------------------
1 | env('cache.driver', 'file'),
10 |
11 | // 缓存连接方式配置
12 | 'stores' => [
13 | 'file' => [
14 | // 驱动方式
15 | 'type' => 'File',
16 | // 缓存保存目录
17 | 'path' => '',
18 | // 缓存前缀
19 | 'prefix' => '',
20 | // 缓存有效期 0表示永久缓存
21 | 'expire' => 0,
22 | // 缓存标签前缀
23 | 'tag_prefix' => 'tag:',
24 | // 序列化机制 例如 ['serialize', 'unserialize']
25 | 'serialize' => [],
26 | ],
27 | // 更多的缓存连接
28 | ],
29 | ];
30 |
--------------------------------------------------------------------------------
/public/router.php:
--------------------------------------------------------------------------------
1 |
10 | // +----------------------------------------------------------------------
11 | // $Id$
12 |
13 | if (is_file($_SERVER["DOCUMENT_ROOT"] . $_SERVER["SCRIPT_NAME"])) {
14 | return false;
15 | } else {
16 | $_SERVER["SCRIPT_FILENAME"] = __DIR__ . '/index.php';
17 |
18 | require __DIR__ . "/index.php";
19 | }
20 |
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 |
10 | // +----------------------------------------------------------------------
11 |
12 | // [ 应用入口文件 ]
13 | namespace think;
14 |
15 | if (version_compare(PHP_VERSION, '8.0.0', '<')) {
16 | die('require PHP >= 8.0 !');
17 | }
18 |
19 | require __DIR__ . '/../vendor/autoload.php';
20 |
21 | // 执行HTTP应用并响应
22 | $http = (new App())->http;
23 |
24 | $response = $http->run();
25 |
26 | $response->send();
27 |
28 | $http->end($response);
29 |
--------------------------------------------------------------------------------
/config/captcha.php:
--------------------------------------------------------------------------------
1 | 4,
9 | // 验证码字符集合
10 | 'codeSet' => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY',
11 | // 验证码过期时间
12 | 'expire' => 1800,
13 | // 是否使用中文验证码
14 | 'useZh' => false,
15 | // 是否使用算术验证码
16 | 'math' => false,
17 | // 是否使用背景图
18 | 'useImgBg' => false,
19 | //验证码字符大小
20 | 'fontSize' => 25,
21 | // 是否使用混淆曲线
22 | 'useCurve' => true,
23 | //是否添加杂点
24 | 'useNoise' => true,
25 | // 验证码字体 不设置则随机
26 | 'fontttf' => '',
27 | //背景颜色
28 | 'bg' => [243, 251, 254],
29 | // 验证码图片高度
30 | 'imageH' => 0,
31 | // 验证码图片宽度
32 | 'imageW' => 0,
33 | // 验证成功后是否重置
34 | 'reset' => true,
35 | // 添加额外的验证码设置
36 | // verify => [
37 | // 'length'=>4,
38 | // ...
39 | //],
40 | ];
41 |
--------------------------------------------------------------------------------
/config/app.php:
--------------------------------------------------------------------------------
1 | env('app.host', ''),
9 | // 应用的命名空间
10 | 'app_namespace' => '',
11 | // 是否启用路由
12 | 'with_route' => true,
13 | // 默认应用
14 | 'default_app' => 'index',
15 | // 默认时区
16 | 'default_timezone' => 'Asia/Shanghai',
17 |
18 | // 应用映射(自动多应用模式有效)
19 | 'app_map' => [],
20 | // 域名绑定(自动多应用模式有效)
21 | 'domain_bind' => [],
22 | // 禁止URL访问的应用列表(自动多应用模式有效)
23 | 'deny_app_list' => [],
24 |
25 | // 异常页面的模板文件
26 | 'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl',
27 |
28 | // 错误显示信息,非调试模式有效
29 | 'error_message' => '页面错误!请稍后再试~',
30 | // 显示错误信息
31 | 'show_error_msg' => true,
32 | 'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl',
33 |
34 | 'version' => '1043',
35 |
36 | 'dbversion' => '1040'
37 | ];
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 消失的彩虹海
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/command/Certtask.php:
--------------------------------------------------------------------------------
1 | setName('certtask')
26 | ->setDescription('SSL证书续签与部署、域名到期提醒、定时切换解析、CF优选IP更新');
27 | }
28 |
29 | protected function execute(Input $input, Output $output)
30 | {
31 | $res = Db::name('config')->cache('configs', 0)->column('value', 'key');
32 | Config::set($res, 'sys');
33 |
34 | (new ScheduleService())->execute();
35 | $res = (new OptimizeService())->execute();
36 | if (!$res) {
37 | (new CertTaskService())->execute();
38 | (new ExpireNoticeService())->task();
39 | }
40 | echo 'done'.PHP_EOL;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/lib/DnsInterface.php:
--------------------------------------------------------------------------------
1 | env('log.channel', 'file'),
9 | // 日志记录级别
10 | 'level' => [],
11 | // 日志类型记录的通道 ['error'=>'email',...]
12 | 'type_channel' => [],
13 | // 关闭全局日志写入
14 | 'close' => false,
15 | // 全局日志处理 支持闭包
16 | 'processor' => null,
17 |
18 | // 日志通道列表
19 | 'channels' => [
20 | 'file' => [
21 | // 日志记录方式
22 | 'type' => 'File',
23 | // 日志保存目录
24 | 'path' => '',
25 | // 单文件日志写入
26 | 'single' => false,
27 | // 独立日志级别
28 | 'apart_level' => [],
29 | // 最大日志文件数量
30 | 'max_files' => 0,
31 | // 使用JSON格式记录
32 | 'json' => false,
33 | // 日志处理
34 | 'processor' => null,
35 | // 关闭通道日志写入
36 | 'close' => false,
37 | // 日志输出格式化
38 | 'format' => '[%s][%s] %s',
39 | // 是否实时写入
40 | 'realtime_write' => false,
41 | ],
42 | // 其它日志通道配置
43 | ],
44 |
45 | ];
46 |
--------------------------------------------------------------------------------
/app/lib/mail/Sendcloud.php:
--------------------------------------------------------------------------------
1 | apiUser = $apiUser;
13 | $this->apiKey = $apiKey;
14 | }
15 | public function send($to, $sub, $msg, $from, $from_name)
16 | {
17 | if (empty($this->apiUser) || empty($this->apiKey)) return false;
18 | $url = 'http://api.sendcloud.net/apiv2/mail/send';
19 | $data = array(
20 | 'apiUser' => $this->apiUser,
21 | 'apiKey' => $this->apiKey,
22 | 'from' => $from,
23 | 'fromName' => $from_name,
24 | 'to' => $to,
25 | 'subject' => $sub,
26 | 'html' => $msg
27 | );
28 | $ch = curl_init($url);
29 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
30 | curl_setopt($ch, CURLOPT_TIMEOUT, 10);
31 | curl_setopt($ch, CURLOPT_POST, 1);
32 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
33 | $json = curl_exec($ch);
34 | curl_close($ch);
35 | $arr = json_decode($json, true);
36 | if ($arr['statusCode'] == 200) {
37 | return true;
38 | } else {
39 | return implode("\n", $arr['message']);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/lib/mail/Aliyun.php:
--------------------------------------------------------------------------------
1 | AccessKeyId = $AccessKeyId;
18 | $this->AccessKeySecret = $AccessKeySecret;
19 | $this->client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $this->Endpoint, $this->Version);
20 | }
21 |
22 | public function send($to, $sub, $msg, $from, $from_name)
23 | {
24 | if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) return false;
25 | $param = [
26 | 'Action' => 'SingleSendMail',
27 | 'AccountName' => $from,
28 | 'ReplyToAddress' => 'false',
29 | 'AddressType' => 1,
30 | 'ToAddress' => $to,
31 | 'FromAlias' => $from_name,
32 | 'Subject' => $sub,
33 | 'HtmlBody' => $msg,
34 | ];
35 | try {
36 | $this->client->request($param);
37 | return true;
38 | } catch (\Exception $e) {
39 | return $e->getMessage();
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/view/domain/log.html:
--------------------------------------------------------------------------------
1 | {extend name="common/layout" /}
2 | {block name="title"}域名日志{/block}
3 | {block name="main"}
4 |
5 |
16 | {/block}
17 | {block name="script"}
18 |
19 |
20 |
21 |
22 |
47 | {/block}
--------------------------------------------------------------------------------
/config/route.php:
--------------------------------------------------------------------------------
1 | '/',
9 | // URL伪静态后缀
10 | 'url_html_suffix' => '',
11 | // URL普通方式参数 用于自动生成
12 | 'url_common_param' => true,
13 | // 是否开启路由延迟解析
14 | 'url_lazy_route' => false,
15 | // 是否强制使用路由
16 | 'url_route_must' => true,
17 | // 合并路由规则
18 | 'route_rule_merge' => false,
19 | // 路由是否完全匹配
20 | 'route_complete_match' => false,
21 | // 访问控制器层名称
22 | 'controller_layer' => 'controller',
23 | // 空控制器名
24 | 'empty_controller' => 'Error',
25 | // 是否使用控制器后缀
26 | 'controller_suffix' => false,
27 | // 默认的路由变量规则
28 | 'default_route_pattern' => '[\w\.]+',
29 | // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则
30 | 'request_cache_key' => false,
31 | // 请求缓存有效期
32 | 'request_cache_expire' => null,
33 | // 全局请求缓存排除规则
34 | 'request_cache_except' => [],
35 | // 默认控制器名
36 | 'default_controller' => 'Index',
37 | // 默认操作名
38 | 'default_action' => 'index',
39 | // 操作方法后缀
40 | 'action_suffix' => '',
41 | // 默认JSONP格式返回的处理方法
42 | 'default_jsonp_handler' => 'jsonpReturn',
43 | // 默认JSONP处理方法
44 | 'var_jsonp_handler' => 'callback',
45 | ];
46 |
--------------------------------------------------------------------------------
/app/ExceptionHandle.php:
--------------------------------------------------------------------------------
1 |
5 |
18 |
19 | {/block}
20 | {block name="script"}
21 |
22 |
39 | {/block}
--------------------------------------------------------------------------------
/app/lib/client/Ucloud.php:
--------------------------------------------------------------------------------
1 | PublicKey = $PublicKey;
18 | $this->PrivateKey = $PrivateKey;
19 | }
20 |
21 | public function request($action, $params)
22 | {
23 | $param = [
24 | 'Action' => $action,
25 | 'PublicKey' => $this->PublicKey,
26 | ];
27 | $param = array_merge($param, $params);
28 | $param['Signature'] = $this->ucloudSignature($param);
29 | $ua = sprintf("PHP/%s PHP-SDK/%s", phpversion(), self::VERSION);
30 | $response = get_curl($this->ApiUrl, json_encode($param), 0, 0, $ua, 0, ['Content-Type' => 'application/json']);
31 | $result = json_decode($response, true);
32 | if (isset($result['RetCode']) && $result['RetCode'] == 0) {
33 | return $result;
34 | } elseif (isset($result['Message'])) {
35 | throw new Exception($result['Message']);
36 | } else {
37 | throw new Exception('返回数据解析失败');
38 | }
39 | }
40 |
41 | private function ucloudSignature($param)
42 | {
43 | ksort($param);
44 | $str = '';
45 | foreach ($param as $key => $value) {
46 | $str .= $key . $value;
47 | }
48 | $str .= $this->PrivateKey;
49 | return sha1($str);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/middleware/AuthApi.php:
--------------------------------------------------------------------------------
1 | -1, 'msg' => '认证参数不能为空'])->code(403);
18 | }
19 | if ($timestamp < time() - 300 || $timestamp > time() + 300) {
20 | return json(['code' => -1, 'msg' => '时间戳不合法'])->code(403);
21 | }
22 | $user = Db::name('user')->where('id', $uid)->find();
23 | if (!$user) {
24 | return json(['code' => -1, 'msg' => '用户不存在'])->code(403);
25 | }
26 | if ($user['status'] == 0) {
27 | return json(['code' => -1, 'msg' => '该用户已被封禁'])->code(403);
28 | }
29 | if ($user['is_api'] == 0) {
30 | return json(['code' => -1, 'msg' => '该用户未开启API权限'])->code(403);
31 | }
32 | if (md5($uid.$timestamp.$user['apikey']) !== $sign) {
33 | return json(['code' => -1, 'msg' => '签名错误'])->code(403);
34 | }
35 |
36 | $user['type'] = 'user';
37 | $user['permission'] = [];
38 | if ($user['level'] == 1) {
39 | $user['permission'] = Db::name('permission')->where('uid', $uid)->column('domain');
40 | }
41 |
42 | $request->islogin = true;
43 | $request->isApi = true;
44 | $request->user = $user;
45 | return $next($request);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/command/Reset.php:
--------------------------------------------------------------------------------
1 | setName('reset')
22 | ->addArgument('type', Argument::REQUIRED, '操作类型,pwd:重置密码,totp:关闭TOTP')
23 | ->addArgument('username', Argument::REQUIRED, '用户名')
24 | ->addArgument('password', Argument::OPTIONAL, '密码')
25 | ->setDescription('重置密码');
26 | }
27 |
28 | protected function execute(Input $input, Output $output)
29 | {
30 | $type = trim($input->getArgument('type'));
31 | $username = trim($input->getArgument('username'));
32 | $user = Db::name('user')->where('username', $username)->find();
33 | if (!$user) {
34 | $output->writeln('用户 ' . $username . ' 不存在');
35 | return;
36 | }
37 | if ($type == 'pwd') {
38 | $password = $input->getArgument('password');
39 | if (empty($password)) $password = '123456';
40 | Db::name('user')->where('id', $user['id'])->update(['password' => password_hash($password, PASSWORD_DEFAULT)]);
41 | $output->writeln('用户 ' . $username . ' 密码重置成功');
42 | } elseif ($type == 'totp') {
43 | Db::name('user')->where('id', $user['id'])->update(['totp_open' => 0, 'totp_secret' => null]);
44 | $output->writeln('用户 ' . $username . ' TOTP关闭成功');
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/middleware/LoadConfig.php:
--------------------------------------------------------------------------------
1 | getRootPath().'.env')) {
24 | if (strpos($request->url(), '/install') === false) {
25 | return redirect((string)url('/install'))->header([
26 | 'Cache-Control' => 'no-store, no-cache, must-revalidate',
27 | 'Pragma' => 'no-cache',
28 | ]);
29 | } else {
30 | return $next($request);
31 | }
32 | }
33 |
34 | try {
35 | $res = Db::name('config')->cache('configs', 0)->column('value', 'key');
36 | if (empty($res['sys_key']) && !empty(env('app.sys_key'))) {
37 | config_set('sys_key', env('app.sys_key'));
38 | Cache::delete('configs');
39 | $res['sys_key'] = env('app.sys_key');
40 | }
41 | Config::set($res, 'sys');
42 | } catch (Exception $e) {
43 | if (!strpos($e->getMessage(), 'doesn\'t exist')) {
44 | throw $e;
45 | }
46 | }
47 |
48 | $request->isApi = false;
49 |
50 | return $next($request)->header([
51 | 'Cache-Control' => 'no-store, no-cache, must-revalidate',
52 | 'Pragma' => 'no-cache',
53 | ]);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/config/database.php:
--------------------------------------------------------------------------------
1 | env('database.driver', 'mysql'),
6 |
7 | // 自定义时间查询规则
8 | 'time_query_rule' => [],
9 |
10 | // 自动写入时间戳字段
11 | // true为自动识别类型 false关闭
12 | // 字符串则明确指定时间字段类型 支持 int timestamp datetime date
13 | 'auto_timestamp' => true,
14 |
15 | // 时间字段取出后的默认时间格式
16 | 'datetime_format' => 'Y-m-d H:i:s',
17 |
18 | // 时间字段配置 配置格式:create_time,update_time
19 | 'datetime_field' => '',
20 |
21 | // 数据库连接配置信息
22 | 'connections' => [
23 | 'mysql' => [
24 | // 数据库类型
25 | 'type' => env('database.type', 'mysql'),
26 | // 服务器地址
27 | 'hostname' => env('database.hostname', '127.0.0.1'),
28 | // 数据库名
29 | 'database' => env('database.database', ''),
30 | // 用户名
31 | 'username' => env('database.username', 'root'),
32 | // 密码
33 | 'password' => env('database.password', ''),
34 | // 端口
35 | 'hostport' => env('database.hostport', '3306'),
36 | // 数据库连接参数
37 | 'params' => [],
38 | // 数据库编码默认采用utf8
39 | 'charset' => env('database.charset', 'utf8'),
40 | // 数据库表前缀
41 | 'prefix' => env('database.prefix', ''),
42 |
43 | // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
44 | 'deploy' => 0,
45 | // 数据库读写是否分离 主从式有效
46 | 'rw_separate' => false,
47 | // 读写分离后 主服务器数量
48 | 'master_num' => 1,
49 | // 指定从服务器序号
50 | 'slave_no' => '',
51 | // 是否严格检查字段是否存在
52 | 'fields_strict' => true,
53 | // 是否需要断线重连
54 | 'break_reconnect' => true,
55 | // 监听SQL
56 | 'trigger_sql' => env('app_debug', true),
57 | // 开启字段缓存
58 | 'fields_cache' => true,
59 | ],
60 |
61 | // 更多的数据库配置信息
62 | ],
63 | ];
64 |
--------------------------------------------------------------------------------
/app/lib/deploy/cachefly.php:
--------------------------------------------------------------------------------
1 | apikey = $config['apikey'];
18 | $this->proxy = $config['proxy'] == 1;
19 | }
20 |
21 | public function check()
22 | {
23 | if (empty($this->apikey)) throw new Exception('API令牌不能为空');
24 | $this->request('/accounts/me');
25 | }
26 |
27 | public function deploy($fullchain, $privatekey, $config, &$info)
28 | {
29 | $params = [
30 | 'certificate' => $fullchain,
31 | 'certificateKey' => $privatekey,
32 | ];
33 | $this->request('/certificates', $params);
34 | $this->log('证书上传成功!');
35 | }
36 |
37 | private function request($path, $params = null, $method = null)
38 | {
39 | $url = $this->url . $path;
40 | $headers = ['x-cf-authorization' => 'Bearer ' . $this->apikey];
41 | $body = null;
42 | if ($params) {
43 | $headers['Content-Type'] = 'application/json';
44 | $body = json_encode($params);
45 | }
46 | $response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
47 | $result = json_decode($response['body'], true);
48 | if ($response['code'] >= 200 && $response['code'] < 300) {
49 | return $result;
50 | } else {
51 | if (!empty($response['body'])) $this->log('Response:' . $response['body']);
52 | throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
53 | }
54 | }
55 |
56 | public function setLogger($func)
57 | {
58 | $this->logger = $func;
59 | }
60 |
61 | private function log($txt)
62 | {
63 | if ($this->logger) {
64 | call_user_func($this->logger, $txt);
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/middleware/AuthUser.php:
--------------------------------------------------------------------------------
1 | where('id', $uid)->find();
22 | if ($user && $user['status'] == 1) {
23 | $session = md5($user['id'].$user['password']);
24 | if ($session === $sid && $expiretime > time()) {
25 | $islogin = true;
26 | }
27 | $user['type'] = 'user';
28 | $user['permission'] = [];
29 | if ($user['level'] == 1) {
30 | $user['permission'] = Db::name('permission')->where('uid', $uid)->column('domain');
31 | }
32 | }
33 | } elseif ($type == 'domain') {
34 | $user = Db::name('domain')->where('id', $uid)->find();
35 | if ($user && $user['is_sso'] == 1) {
36 | $session = md5($user['id'].$user['name']);
37 | if ($session === $sid && $expiretime > time()) {
38 | $islogin = true;
39 | }
40 | $user['username'] = $user['name'];
41 | $user['regtime'] = $user['addtime'];
42 | $user['type'] = 'domain';
43 | $user['level'] = 0;
44 | $user['permission'] = [$user['name']];
45 | }
46 | }
47 | }
48 | }
49 | $request->islogin = $islogin;
50 | $request->user = $user;
51 | return $next($request);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/lib/deploy/rainyun.php:
--------------------------------------------------------------------------------
1 | apikey = $config['apikey'];
18 | $this->proxy = $config['proxy'] == 1;
19 | }
20 |
21 | public function check()
22 | {
23 | if (empty($this->apikey)) throw new Exception('ApiKey不能为空');
24 | $this->request('/product/');
25 | }
26 |
27 | public function deploy($fullchain, $privatekey, $config, &$info)
28 | {
29 | if (empty($config['id'])) throw new Exception('证书ID不能为空');
30 |
31 | $params = [
32 | 'cert' => $fullchain,
33 | 'key' => $privatekey,
34 | ];
35 | try {
36 | $this->request('/product/sslcenter/' . $config['id'], $params, 'PUT');
37 | } catch (Exception $e) {
38 | throw new Exception($e->getMessage());
39 | }
40 |
41 | $this->log('证书ID:' . $config['id'] . '更新成功!');
42 | }
43 |
44 | private function request($path, $params = null, $method = null)
45 | {
46 | $url = $this->url . $path;
47 | $headers = [
48 | 'x-api-key' => $this->apikey,
49 | ];
50 | $body = null;
51 | if ($params) {
52 | $headers['Content-Type'] = 'application/json';
53 | $body = json_encode($params);
54 | }
55 | $response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
56 | $result = json_decode($response['body'], true);
57 | if (isset($result['code']) && $result['code'] == 200) {
58 | return $result;
59 | } elseif (isset($result['message'])) {
60 | throw new Exception($result['message']);
61 | } else {
62 | if (!empty($response['body'])) $this->log('Response:' . $response['body']);
63 | throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
64 | }
65 | }
66 |
67 | public function setLogger($func)
68 | {
69 | $this->logger = $func;
70 | }
71 |
72 | private function log($txt)
73 | {
74 | if ($this->logger) {
75 | call_user_func($this->logger, $txt);
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/view/user/log.html:
--------------------------------------------------------------------------------
1 | {extend name="common/layout" /}
2 | {block name="title"}操作日志{/block}
3 | {block name="main"}
4 |
5 |
27 | {/block}
28 | {block name="script"}
29 |
30 |
31 |
32 |
33 |
77 | {/block}
--------------------------------------------------------------------------------
/app/view/dmonitor/taskinfo.html:
--------------------------------------------------------------------------------
1 | {extend name="common/layout" /}
2 | {block name="title"}切换记录{/block}
3 | {block name="main"}
4 |
7 |
30 | {/block}
31 | {block name="script"}
32 |
33 |
34 |
35 |
36 |
73 | {/block}
--------------------------------------------------------------------------------
/app/lib/deploy/gcore.php:
--------------------------------------------------------------------------------
1 | apikey = $config['apikey'];
18 | $this->proxy = $config['proxy'] == 1;
19 | }
20 |
21 | public function check()
22 | {
23 | if (empty($this->apikey)) throw new Exception('API令牌不能为空');
24 | $this->request('/iam/clients/me');
25 | }
26 |
27 | public function deploy($fullchain, $privatekey, $config, &$info)
28 | {
29 | $id = $config['id'];
30 | if (empty($id)) throw new Exception('证书ID不能为空');
31 |
32 | $params = [
33 | 'name' => $config['name'],
34 | 'sslCertificate' => $fullchain,
35 | 'sslPrivateKey' => $privatekey,
36 | 'validate_root_ca' => true,
37 | ];
38 | $this->request('/cdn/sslData/' . $id, $params, 'PUT');
39 | $this->log('证书ID:' . $id . '更新成功!');
40 | }
41 |
42 | private function request($path, $params = null, $method = null)
43 | {
44 | $url = $this->url . $path;
45 | $headers = ['Authorization' => 'APIKey ' . $this->apikey];
46 | $body = null;
47 | if ($params) {
48 | $headers['Content-Type'] = 'application/json';
49 | $body = json_encode($params);
50 | }
51 | $response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
52 | $result = json_decode($response['body'], true);
53 | if ($response['code'] >= 200 && $response['code'] < 300) {
54 | return $result;
55 | } elseif (isset($result['message']['message'])) {
56 | throw new Exception($result['message']['message']);
57 | } elseif (isset($result['errors'])) {
58 | $errors = $result['errors'][array_key_first($result['errors'])];
59 | throw new Exception($errors[0]);
60 | } else {
61 | if (!empty($response['body'])) $this->log('Response:' . $response['body']);
62 | throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
63 | }
64 | }
65 |
66 | public function setLogger($func)
67 | {
68 | $this->logger = $func;
69 | }
70 |
71 | private function log($txt)
72 | {
73 | if ($this->logger) {
74 | call_user_func($this->logger, $txt);
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/utils/DnsQueryUtils.php:
--------------------------------------------------------------------------------
1 | DNS_A, 'AAAA' => DNS_AAAA, 'CNAME' => DNS_CNAME, 'MX' => DNS_MX, 'TXT' => DNS_TXT];
14 | if (!array_key_exists($type, $dns_type)) return false;
15 | try{
16 | $list = dns_get_record($domain, $dns_type[$type]);
17 | }catch(Exception $e){
18 | return false;
19 | }
20 | if (!$list || empty($list)) return false;
21 | $result = [];
22 | foreach ($list as $row) {
23 | if ($row['type'] == 'A') {
24 | $result[] = $row['ip'];
25 | } elseif ($row['type'] == 'AAAA') {
26 | $result[] = $row['ipv6'];
27 | } elseif ($row['type'] == 'CNAME') {
28 | $result[] = $row['target'];
29 | } elseif ($row['type'] == 'MX') {
30 | $result[] = $row['target'];
31 | } elseif ($row['type'] == 'TXT') {
32 | $result[] = $row['txt'];
33 | }
34 | }
35 | return $result;
36 | }
37 |
38 | public static function query_dns_doh($domain, $type)
39 | {
40 | $dns_type = ['A' => 1, 'AAAA' => 28, 'CNAME' => 5, 'MX' => 15, 'TXT' => 16, 'SOA' => 6, 'NS' => 2, 'PTR' => 12, 'SRV' => 33, 'CAA' => 257];
41 | if (!array_key_exists($type, $dns_type)) return false;
42 | $id = array_rand(self::$doh_servers);
43 | $url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type];
44 | $data = get_curl($url);
45 | $arr = json_decode($data, true);
46 | if (!$arr) {
47 | unset(self::$doh_servers[$id]);
48 | $id = array_rand(self::$doh_servers);
49 | $url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type];
50 | $data = get_curl($url);
51 | $arr = json_decode($data, true);
52 | if (!$arr) return false;
53 | }
54 | $result = [];
55 | if (isset($arr['Answer'])) {
56 | foreach ($arr['Answer'] as $row) {
57 | $value = $row['data'];
58 | if ($row['type'] == 5) $value = trim($value, '.');
59 | if ($row['type'] == 16) $value = trim($value, '"');
60 | $result[] = $value;
61 | }
62 | }
63 | return $result;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/public/static/js/html5shiv-3.7.3.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
3 | */
4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document);
--------------------------------------------------------------------------------
/public/static/js/FileSaver-2.0.5.min.js:
--------------------------------------------------------------------------------
1 | (function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.navigator&&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
2 |
3 | //# sourceMappingURL=FileSaver.min.js.map
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://getcomposer.org/schema.json",
3 | "name": "netcccyun/dnsmgr",
4 | "description": "聚合DNS管理系统",
5 | "type": "project",
6 | "keywords": [
7 | "thinkphp",
8 | "dns",
9 | "dnsmanager",
10 | "cccyun"
11 | ],
12 | "homepage": "https://blog.cccyun.cn/post-526.html",
13 | "license": "Apache-2.0",
14 | "authors": [
15 | {
16 | "name": "liu21st",
17 | "email": "liu21st@gmail.com",
18 | "role": "Framework Developer"
19 | },
20 | {
21 | "name": "yunwuxin",
22 | "email": "448901948@qq.com",
23 | "role": "Framework Developer"
24 | },
25 | {
26 | "name": "netcccyun",
27 | "homepage": "https://blog.cccyun.cn",
28 | "role": "Project Owner"
29 | },
30 | {
31 | "name": "coolxitech",
32 | "email": "admin@kuxi.tech",
33 | "homepage": "https://www.kuxi.tech",
34 | "role": "Project Developer"
35 | },
36 | {
37 | "name": "耗子",
38 | "email": "haozi@loli.email",
39 | "homepage": "https://hzbk.net",
40 | "role": "Project Developer"
41 | }
42 | ],
43 | "require": {
44 | "php": ">=8.2.0",
45 | "ext-curl": "*",
46 | "ext-ftp": "*",
47 | "ext-gd": "*",
48 | "ext-mbstring": "*",
49 | "ext-openssl": "*",
50 | "ext-pdo": "*",
51 | "ext-sockets": "*",
52 | "ext-ssh2": "*",
53 | "cccyun/php-whois": "^1.0",
54 | "cccyun/think-captcha": "^3.0",
55 | "guzzlehttp/guzzle": "^7.0",
56 | "phpmailer/phpmailer": "^7.0",
57 | "symfony/polyfill-intl-idn": "^1.32",
58 | "symfony/polyfill-mbstring": "^1.32",
59 | "symfony/polyfill-php81": "^1.32",
60 | "symfony/polyfill-php82": "^1.32",
61 | "symfony/yaml": "^7.3",
62 | "topthink/framework": "^8.1.0",
63 | "topthink/think-orm": "^4.0",
64 | "topthink/think-view": "^2.0"
65 | },
66 | "require-dev": {
67 | "symfony/var-dumper": "^7.3",
68 | "topthink/think-trace":"^2.0",
69 | "swoole/ide-helper": "^6.0"
70 | },
71 | "autoload": {
72 | "psr-4": {
73 | "app\\": "app"
74 | }
75 | },
76 | "config": {
77 | "optimize-autoloader": true,
78 | "sort-packages": true,
79 | "platform-check": false,
80 | "preferred-install": "dist"
81 | },
82 | "scripts": {
83 | "post-autoload-dump": [
84 | "@php think service:discover",
85 | "@php think vendor:publish"
86 | ]
87 | },
88 | "minimum-stability": "stable",
89 | "prefer-stable": true
90 | }
91 |
--------------------------------------------------------------------------------
/app/lib/deploy/baishan.php:
--------------------------------------------------------------------------------
1 | token = $config['token'];
18 | $this->proxy = $config['proxy'] == 1;
19 | }
20 |
21 | public function check()
22 | {
23 | if (empty($this->token)) throw new Exception('token不能为空');
24 | }
25 |
26 | public function deploy($fullchain, $privatekey, $config, &$info)
27 | {
28 | if (empty($config['id'])) throw new Exception('证书ID不能为空');
29 |
30 | $certInfo = openssl_x509_parse($fullchain, true);
31 | if (!$certInfo) throw new Exception('证书解析失败');
32 | $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
33 |
34 | $params = [
35 | 'cert_id' => $config['id'],
36 | 'name' => $cert_name,
37 | 'certificate' => $fullchain,
38 | 'key' => $privatekey,
39 | ];
40 | try {
41 | $this->request('/v2/domain/certificate?token=' . $this->token, $params);
42 | } catch (Exception $e) {
43 | if (strpos($e->getMessage(), 'this certificate is exists') !== false) {
44 | $this->log('证书ID:' . $config['id'] . '已存在,无需更新');
45 | return;
46 | }
47 | throw new Exception($e->getMessage());
48 | }
49 |
50 | $this->log('证书ID:' . $config['id'] . '更新成功!');
51 | }
52 |
53 | private function request($path, $params = null)
54 | {
55 | $url = $this->url . $path;
56 | $headers = [];
57 | $body = null;
58 | if ($params) {
59 | $headers['Content-Type'] = 'application/json';
60 | $body = json_encode($params);
61 | }
62 | $response = http_request($url, $body, null, null, $headers, $this->proxy);
63 | $result = json_decode($response['body'], true);
64 | if (isset($result['code']) && $result['code'] == 0) {
65 | return $result;
66 | } elseif (isset($result['message'])) {
67 | throw new Exception($result['message']);
68 | } else {
69 | if (!empty($response['body'])) $this->log('Response:' . $response['body']);
70 | throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
71 | }
72 | }
73 |
74 | public function setLogger($func)
75 | {
76 | $this->logger = $func;
77 | }
78 |
79 | private function log($txt)
80 | {
81 | if ($this->logger) {
82 | call_user_func($this->logger, $txt);
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/command/Dmtask.php:
--------------------------------------------------------------------------------
1 | setName('dmtask')
23 | ->setDescription('容灾切换任务');
24 | }
25 |
26 | protected function execute(Input $input, Output $output)
27 | {
28 | $res = Db::name('config')->cache('configs', 0)->column('value', 'key');
29 | Config::set($res, 'sys');
30 |
31 | config_set('run_error', '');
32 | if (!extension_loaded('swoole')) {
33 | $output->writeln('[Error] 未安装Swoole扩展');
34 | config_set('run_error', '未安装Swoole扩展');
35 | return;
36 | }
37 | try {
38 | $output->writeln('进程启动成功.');
39 | $this->runtask();
40 | } catch (Exception $e) {
41 | $output->writeln('[Error] ' . $e->getMessage());
42 | config_set('run_error', $e->getMessage());
43 | }
44 | }
45 |
46 | private function runtask()
47 | {
48 | \Co::set(['hook_flags' => SWOOLE_HOOK_ALL]);
49 | \Co\run(function () {
50 | $date = date("Ymd");
51 | $count = config_get('run_count', null, true) ?? 0;
52 | while (true) {
53 | sleep(1);
54 | if ($date != date("Ymd")) {
55 | $count = 0;
56 | $date = date("Ymd");
57 | }
58 |
59 | $rows = Db::name('dmtask')->where('checknexttime', '<=', time())->where('active', 1)->order('id', 'ASC')->select();
60 | foreach ($rows as $row) {
61 | \go(function () use ($row) {
62 | try {
63 | (new TaskRunner())->execute($row);
64 | } catch (\Swoole\ExitException $e) {
65 | echo $e->getStatus() . "\n";
66 | } catch (Exception $e) {
67 | echo $e->__toString() . "\n";
68 | }
69 | });
70 | Db::name('dmtask')->where('id', $row['id'])->update([
71 | 'checktime' => time(),
72 | 'checknexttime' => time() + $row['frequency']
73 | ]);
74 | $count++;
75 | }
76 |
77 | config_set('run_time', date("Y-m-d H:i:s"));
78 | config_set('run_count', $count);
79 | }
80 | });
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/lib/deploy/local.php:
--------------------------------------------------------------------------------
1 | log('证书已保存到:' . $config['pem_cert_file']);
28 | } else {
29 | throw new Exception('证书保存到' . $config['pem_cert_file'] . '失败,请检查目录权限');
30 | }
31 | if (file_put_contents($config['pem_key_file'], $privatekey)) {
32 | $this->log('私钥已保存到:' . $config['pem_key_file']);
33 | } else {
34 | throw new Exception('私钥保存到' . $config['pem_key_file'] . '失败,请检查目录权限');
35 | }
36 | } elseif ($config['format'] == 'pfx') {
37 | $dir = dirname($config['pfx_file']);
38 | if (!is_dir($dir)) throw new Exception($dir . ' 目录不存在');
39 | if (!is_writable($dir)) throw new Exception($dir . ' 目录不可写');
40 |
41 | $pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null);
42 | if (file_put_contents($config['pfx_file'], $pfx)) {
43 | $this->log('PFX证书已保存到:' . $config['pfx_file']);
44 | } else {
45 | throw new Exception('PFX证书保存到' . $config['pfx_file'] . '失败,请检查目录权限');
46 | }
47 | }
48 | if (!empty($config['cmd'])) {
49 | $cmds = explode("\n", $config['cmd']);
50 | foreach ($cmds as $cmd) {
51 | $cmd = trim($cmd);
52 | if (empty($cmd)) continue;
53 | $this->log('执行命令:' . $cmd);
54 | $output = [];
55 | $ret = 0;
56 | exec($cmd, $output, $ret);
57 | if ($ret == 0) {
58 | $this->log('执行命令成功:' . implode("\n", $output));
59 | } else {
60 | throw new Exception('执行命令失败:' . implode("\n", $output));
61 | }
62 | }
63 | }
64 | }
65 |
66 | private function log($txt)
67 | {
68 | if ($this->logger) {
69 | call_user_func($this->logger, $txt);
70 | }
71 | }
72 |
73 | public function setLogger($logger)
74 | {
75 | $this->logger = $logger;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/public/static/images/success.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/lib/deploy/kuocai.php:
--------------------------------------------------------------------------------
1 | username = $config['username'];
19 | $this->password = $config['password'];
20 | $this->proxy = $config['proxy'] == 1;
21 | }
22 |
23 | public function check()
24 | {
25 | if (empty($this->username) || empty($this->password)) {
26 | throw new Exception('请填写控制台账号和密码');
27 | }
28 | $this->request('/login/loginUser', [
29 | 'userAccount' => $this->username,
30 | 'userPwd' => $this->password,
31 | 'remember' => 'true'
32 | ]);
33 | }
34 |
35 | public function deploy($fullchain, $privatekey, $config, &$info)
36 | {
37 | $id = $config['id'];
38 | if (empty($id)) {
39 | throw new Exception('域名ID不能为空');
40 | }
41 | $this->token = $this->request('/login/loginUser', [
42 | 'userAccount' => $this->username,
43 | 'userPwd' => $this->password,
44 | 'remember' => 'true'
45 | ]);
46 | $this->request('/CdnDomainHttps/httpsConfiguration', [
47 | 'doMainId' => $id,
48 | 'https' => [
49 | 'certificate_name' => uniqid('cert_'),
50 | 'certificate_source' => '0',
51 | 'certificate_value' => $fullchain,
52 | 'https_status' => 'on',
53 | 'private_key' => $privatekey,
54 | ]
55 | ], true);
56 | $this->log("域名ID:{$id}更新成功!");
57 | }
58 |
59 | public function setLogger($func)
60 | {
61 | $this->logger = $func;
62 | }
63 |
64 | private function log($txt)
65 | {
66 | if ($this->logger) {
67 | call_user_func($this->logger, $txt);
68 | }
69 | }
70 |
71 | private function request($path, $params = null, $json = false)
72 | {
73 | $url = 'https://kuocai.cn' . $path;
74 | $body = $json ? json_encode($params) : $params;
75 | $headers = [];
76 | if ($json) $headers['Content-Type'] = 'application/json';
77 | $response = http_request(
78 | $url,
79 | $body,
80 | null,
81 | $this->token ? "kuocai_cdn_token={$this->token}" : null,
82 | $headers,
83 | $this->proxy
84 | );
85 | $result = json_decode($response['body'], true);
86 | if (isset($result['code']) && $result['code'] == 'SUCCESS') {
87 | return isset($result['data']) ? $result['data'] : null;
88 | } elseif (isset($result['message'])) {
89 | throw new Exception($result['message']);
90 | } else {
91 | throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/app/view/dispatch_jump.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 温馨提示
6 |
7 |
8 |
28 |
29 |
30 |
31 |
32 |

33 |
34 |
{$msg}
35 | {if $url}
36 |
37 | 页面将在 {$wait} 秒后自动跳转
38 |
39 | {/if}
40 |
41 | 返回上一页
42 | {if $url}
43 | 立即跳转
44 | {/if}
45 |
46 |
47 |
59 |
60 |
--------------------------------------------------------------------------------
/app/lib/deploy/ksyun.php:
--------------------------------------------------------------------------------
1 | AccessKeyId = $config['AccessKeyId'];
19 | $this->SecretAccessKey = $config['SecretAccessKey'];
20 | $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
21 | }
22 |
23 | public function check()
24 | {
25 | if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空');
26 | $client = new KsyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cdn.api.ksyun.com', 'cdn', 'cn-shanghai-2', $this->proxy);
27 | $client->request('GET', 'GetCertificates', '2016-09-01', '/2016-09-01/cert/GetCertificates');
28 | return true;
29 | }
30 |
31 | public function deploy($fullchain, $privatekey, $config, &$info)
32 | {
33 | $this->deploy_cdn($fullchain, $privatekey, $config, $info);
34 | }
35 |
36 | public function deploy_cdn($fullchain, $privatekey, $config, &$info)
37 | {
38 | if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
39 | $certInfo = openssl_x509_parse($fullchain, true);
40 | if (!$certInfo) throw new Exception('证书解析失败');
41 | $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
42 | $domains = explode(',', $config['domain']);
43 |
44 | $client = new KsyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cdn.api.ksyun.com', 'cdn', 'cn-shanghai-2', $this->proxy);
45 | $param = [
46 | 'PageSize' => 100,
47 | 'PageNumber' => 1,
48 | ];
49 | $domain_ids = [];
50 | $result = $client->request('GET', 'GetCdnDomains', '2019-06-01', '/2019-06-01/domain/GetCdnDomains', $param);
51 | foreach ($result['Domains'] as $row) {
52 | if (in_array($row['DomainName'], $domains)) {
53 | $domain_ids[] = $row['DomainId'];
54 | }
55 | }
56 | if (count($domain_ids) == 0) throw new Exception('未找到对应的CDN域名');
57 | $param = [
58 | 'Enable' => 'on',
59 | 'DomainIds' => implode(',', $domain_ids),
60 | 'CertificateName' => $config['cert_name'],
61 | 'ServerCertificate' => $fullchain,
62 | 'PrivateKey' => $privatekey,
63 | ];
64 | $result = $client->request('POST', 'ConfigCertificate', '2016-09-01', '/2016-09-01/cert/ConfigCertificate', $param);
65 | $this->log('CDN证书部署成功,证书ID:' . $result['CertificateId']);
66 | }
67 |
68 | public function setLogger($func)
69 | {
70 | $this->logger = $func;
71 | }
72 |
73 | private function log($txt)
74 | {
75 | if ($this->logger) {
76 | call_user_func($this->logger, $txt);
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/view/cert/certaccount.html:
--------------------------------------------------------------------------------
1 | {extend name="common/layout" /}
2 | {block name="title"}SSL证书账户管理{/block}
3 | {block name="main"}
4 |
25 | {/block}
26 | {block name="script"}
27 |
28 |
29 |
30 |
31 |
93 | {/block}
--------------------------------------------------------------------------------
/app/view/cert/deployaccount.html:
--------------------------------------------------------------------------------
1 | {extend name="common/layout" /}
2 | {block name="title"}自动部署任务账户管理{/block}
3 | {block name="main"}
4 |
25 | {/block}
26 | {block name="script"}
27 |
28 |
29 |
30 |
31 |
93 | {/block}
--------------------------------------------------------------------------------
/public/static/js/layer/mobile/layer.js:
--------------------------------------------------------------------------------
1 | /*! layer mobile-v2.0.0 Web 通用弹出层组件 MIT License */
2 | ;!function(e){"use strict";var t=document,n="querySelectorAll",i="getElementsByClassName",a=function(e){return t[n](e)},s={type:0,shade:!0,shadeClose:!0,fixed:!0,anim:"scale"},l={extend:function(e){var t=JSON.parse(JSON.stringify(s));for(var n in e)t[n]=e[n];return t},timer:{},end:{}};l.touch=function(e,t){e.addEventListener("click",function(e){t.call(this,e)},!1)};var r=0,o=["layui-m-layer"],c=function(e){var t=this;t.config=l.extend(e),t.view()};c.prototype.view=function(){var e=this,n=e.config,s=t.createElement("div");e.id=s.id=o[0]+r,s.setAttribute("class",o[0]+" "+o[0]+(n.type||0)),s.setAttribute("index",r);var l=function(){var e="object"==typeof n.title;return n.title?''+(e?n.title[0]:n.title)+"
":""}(),c=function(){"string"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e=''+n.btn[0]+"",2===t&&(e=''+n.btn[1]+""+e),''+e+"
"):""}();if(n.fixed||(n.top=n.hasOwnProperty("top")?n.top:100,n.style=n.style||"",n.style+=" top:"+(t.body.scrollTop+n.top)+"px"),2===n.type&&(n.content=''+(n.content||"")+"
"),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"':"")+'",!n.type||2===n.type){var d=t[i](o[0]+n.type),y=d.length;y>=1&&layer.close(d[0].getAttribute("index"))}document.body.appendChild(s);var u=e.elem=a("#"+e.id)[0];n.success&&n.success(u),e.index=r++,e.action(n,u)},c.prototype.action=function(e,t){var n=this;e.time&&(l.timer[n.index]=setTimeout(function(){layer.close(n.index)},1e3*e.time));var a=function(){var t=this.getAttribute("type");0==t?(e.no&&e.no(),layer.close(n.index)):e.yes?e.yes(n.index):layer.close(n.index)};if(e.btn)for(var s=t[i]("layui-m-layerbtn")[0].children,r=s.length,o=0;ourl = rtrim($config['url'], '/');
19 | $this->api_user = $config['api_user'];
20 | $this->api_key = $config['api_key'];
21 | $this->proxy = $config['proxy'] == 1;
22 | }
23 |
24 | public function check()
25 | {
26 | if (empty($this->url) || empty($this->api_user) || empty($this->api_key)) throw new Exception('必填内容不能为空');
27 |
28 | $path = '/api2/json/access';
29 | $this->send_request($path);
30 | }
31 |
32 | public function deploy($fullchain, $privatekey, $config, &$info)
33 | {
34 | if (empty($config['node'])) throw new Exception('节点名称不能为空');
35 | $cert_hash = openssl_x509_fingerprint($fullchain, 'sha256');
36 | if (!$cert_hash) throw new Exception('证书解析失败');
37 |
38 | $path = '/api2/json/nodes/' . $config['node'] . '/certificates/info';
39 | $list = $this->send_request($path);
40 | foreach ($list as $item) {
41 | $fingerprint = strtolower(str_replace(':', '', $item['fingerprint']));
42 | if ($fingerprint == $cert_hash) {
43 | $this->log('节点:' . $config['node'] . ' 证书已存在');
44 | return;
45 | }
46 | }
47 |
48 | $path = '/api2/json/nodes/' . $config['node'] . '/certificates/custom';
49 | $params = [
50 | 'certificates' => $fullchain,
51 | 'key' => $privatekey,
52 | 'force' => 1,
53 | 'restart' => 1,
54 | ];
55 | $this->send_request($path, $params);
56 | $this->log('节点:' . $config['node'] . ' 证书部署成功!');
57 | }
58 |
59 | private function send_request($path, $params = null)
60 | {
61 | $url = $this->url . $path;
62 | $headers = ['Authorization' => 'PVEAPIToken=' . $this->api_user . '=' . $this->api_key];
63 | $post = $params ? http_build_query($params) : null;
64 | $response = http_request($url, $post, null, null, $headers, $this->proxy);
65 | if ($response['code'] == 200) {
66 | $result = json_decode($response['body'], true);
67 | if (isset($result['data'])) {
68 | return $result['data'];
69 | } elseif (isset($result['errors'])) {
70 | if (is_array($result['errors'])) {
71 | $result['errors'] = implode(';', $result['errors']);
72 | }
73 | throw new Exception($result['errors']);
74 | } else {
75 | throw new Exception('返回数据解析失败');
76 | }
77 | } else {
78 | throw new Exception('请求失败(httpCode=' . $response['code'] . ', body=' . $response['body'] . ')');
79 | }
80 | }
81 |
82 | public function setLogger($func)
83 | {
84 | $this->logger = $func;
85 | }
86 |
87 | private function log($txt)
88 | {
89 | if ($this->logger) {
90 | call_user_func($this->logger, $txt);
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/public/static/images/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/lib/deploy/uusec.php:
--------------------------------------------------------------------------------
1 | url = rtrim($config['url'], '/');
20 | $this->username = $config['username'];
21 | $this->password = $config['password'];
22 | $this->proxy = $config['proxy'] == 1;
23 | }
24 |
25 | public function check()
26 | {
27 | if (empty($this->url) || empty($this->password) || empty($this->password)) throw new Exception('用户名和密码不能为空');
28 | $this->login();
29 | }
30 |
31 | public function deploy($fullchain, $privatekey, $config, &$info)
32 | {
33 | $id = $config['id'];
34 | if (empty($id)) throw new Exception('证书ID不能为空');
35 |
36 | $this->login();
37 |
38 | $params = [
39 | 'id' => intval($id),
40 | 'type' => 1,
41 | 'name' => $config['name'],
42 | 'crt' => $fullchain,
43 | 'key' => $privatekey,
44 | ];
45 | $result = $this->request('/api/v1/certs', $params, 'PUT');
46 | if (is_string($result) && $result == 'OK') {
47 | $this->log('证书ID:' . $id . '更新成功!');
48 | } else {
49 | throw new Exception('证书ID:' . $id . '更新失败,' . (isset($result['err']) ? $result['err'] : '未知错误'));
50 | }
51 | }
52 |
53 | private function login()
54 | {
55 | $path = '/api/v1/users/login';
56 | $params = [
57 | 'usr' => $this->username,
58 | 'pwd' => $this->password,
59 | 'otp' => '',
60 | ];
61 | $result = $this->request($path, $params);
62 | if (isset($result['token'])) {
63 | $this->accessToken = $result['token'];
64 | } else {
65 | throw new Exception('登录失败,' . (isset($result['err']) ? $result['err'] : '未知错误'));
66 | }
67 | }
68 |
69 | private function request($path, $params = null, $method = null)
70 | {
71 | $url = $this->url . $path;
72 | $headers = [];
73 | $body = null;
74 | if ($this->accessToken) {
75 | $headers['Authorization'] = 'Bearer ' . $this->accessToken;
76 | }
77 | if ($params) {
78 | $headers['Content-Type'] = 'application/json;charset=UTF-8';
79 | $body = json_encode($params);
80 | }
81 | $response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
82 | $result = json_decode($response['body'], true);
83 | if ($response['code'] == 200) {
84 | return $result;
85 | } elseif (isset($result['message'])) {
86 | throw new Exception($result['message']);
87 | } else {
88 | throw new Exception('请求失败,HTTP状态码:' . $response['code']);
89 | }
90 | }
91 |
92 | public function setLogger($func)
93 | {
94 | $this->logger = $func;
95 | }
96 |
97 | private function log($txt)
98 | {
99 | if ($this->logger) {
100 | call_user_func($this->logger, $txt);
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/app/view/domain/expire_notice.html:
--------------------------------------------------------------------------------
1 | {extend name="common/layout" /}
2 | {block name="title"}域名到期提醒设置{/block}
3 | {block name="main"}
4 |
45 | {/block}
46 | {block name="script"}
47 |
48 |
81 | {/block}
--------------------------------------------------------------------------------
/app/BaseController.php:
--------------------------------------------------------------------------------
1 | app = $app;
50 | $this->request = $this->app->request;
51 |
52 | // 控制器初始化
53 | $this->initialize();
54 | }
55 |
56 | // 初始化
57 | protected function initialize()
58 | {
59 | $this->clientip = real_ip(env('app.ip_type', 0));
60 | }
61 |
62 | /**
63 | * 验证数据
64 | * @access protected
65 | * @param array $data 数据
66 | * @param string|array $validate 验证器名或者验证规则数组
67 | * @param array $message 提示信息
68 | * @param bool $batch 是否批量验证
69 | * @return array|string|true
70 | * @throws ValidateException
71 | */
72 | protected function validate(array $data, $validate, array $message = [], bool $batch = false)
73 | {
74 | if (is_array($validate)) {
75 | $v = new Validate();
76 | $v->rule($validate);
77 | } else {
78 | if (strpos($validate, '.')) {
79 | // 支持场景
80 | [$validate, $scene] = explode('.', $validate);
81 | }
82 | $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
83 | $v = new $class();
84 | if (!empty($scene)) {
85 | $v->scene($scene);
86 | }
87 | }
88 |
89 | $v->message($message);
90 |
91 | // 是否批量验证
92 | if ($batch || $this->batchValidate) {
93 | $v->batch(true);
94 | }
95 |
96 | return $v->failException(true)->check($data);
97 | }
98 |
99 |
100 | protected function alert($code, $msg = '', $url = null, $wait = 3)
101 | {
102 | if ($url) {
103 | $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : (string)$this->app->route->buildUrl($url);
104 | }
105 | if (empty($msg)) {
106 | $msg = '未知错误';
107 | }
108 |
109 | if ($this->request->isApi) {
110 | return json(['code' => $code == 'success' ? 0 : -1, 'msg' => $msg]);
111 | }
112 | if ($this->request->isAjax()) {
113 | return json(['code' => $code == 'success' ? 0 : -1, 'msg' => $msg, 'url' => $url]);
114 | }
115 |
116 | View::assign([
117 | 'code' => $code,
118 | 'msg' => $msg,
119 | 'url' => $url,
120 | 'wait' => $wait,
121 | ]);
122 | return View::fetch(app()->getBasePath().'view/dispatch_jump.html');
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/app/lib/client/Aliyun.php:
--------------------------------------------------------------------------------
1 | AccessKeyId = $AccessKeyId;
21 | $this->AccessKeySecret = $AccessKeySecret;
22 | $this->Endpoint = $Endpoint;
23 | $this->Version = $Version;
24 | $this->proxy = $proxy;
25 | }
26 |
27 | /**
28 | * @param array $param 请求参数
29 | * @return bool|array
30 | * @throws Exception
31 | */
32 | public function request($param, $method = 'POST')
33 | {
34 | $url = 'https://' . $this->Endpoint . '/';
35 | $data = array(
36 | 'Format' => 'JSON',
37 | 'Version' => $this->Version,
38 | 'AccessKeyId' => $this->AccessKeyId,
39 | 'SignatureMethod' => 'HMAC-SHA1',
40 | 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
41 | 'SignatureVersion' => '1.0',
42 | 'SignatureNonce' => random(8)
43 | );
44 | $data = array_merge($data, $param);
45 | $data['Signature'] = $this->aliyunSignature($data, $this->AccessKeySecret, $method);
46 | if ($method == 'GET') {
47 | $url .= '?' . http_build_query($data);
48 | }
49 | $ch = curl_init($url);
50 | if ($this->proxy) {
51 | curl_set_proxy($ch);
52 | }
53 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
54 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
55 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
56 | curl_setopt($ch, CURLOPT_TIMEOUT, 10);
57 | if ($method == 'POST') {
58 | curl_setopt($ch, CURLOPT_POST, 1);
59 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
60 | }
61 | $response = curl_exec($ch);
62 | $errno = curl_errno($ch);
63 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
64 | if ($errno) {
65 | $errmsg = curl_error($ch);
66 | curl_close($ch);
67 | throw new Exception('Curl error: ' . $errmsg);
68 | }
69 | curl_close($ch);
70 |
71 | $arr = json_decode($response, true);
72 | if ($httpCode == 200) {
73 | return $arr;
74 | } elseif ($arr) {
75 | throw new Exception($arr['Message']);
76 | } else {
77 | throw new Exception('返回数据解析失败');
78 | }
79 | }
80 |
81 | private function aliyunSignature($parameters, $accessKeySecret, $method)
82 | {
83 | ksort($parameters);
84 | $canonicalizedQueryString = '';
85 | foreach ($parameters as $key => $value) {
86 | if ($value === null) continue;
87 | $canonicalizedQueryString .= '&' . $this->percentEncode($key) . '=' . $this->percentEncode($value);
88 | }
89 | $stringToSign = $method . '&%2F&' . $this->percentEncode(substr($canonicalizedQueryString, 1));
90 | $signature = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&", true));
91 |
92 | return $signature;
93 | }
94 |
95 | private function percentEncode($str)
96 | {
97 | $search = ['+', '*', '%7E'];
98 | $replace = ['%20', '%2A', '~'];
99 | return str_replace($search, $replace, urlencode($str));
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/lib/deploy/xp.php:
--------------------------------------------------------------------------------
1 | url = rtrim($config['url'], '/');
18 | $this->apikey = $config['apikey'];
19 | $this->proxy = $config['proxy'] == 1;
20 | }
21 |
22 | public function check()
23 | {
24 | if (empty($this->url) || empty($this->apikey)) throw new Exception('请填写面板地址和接口密钥');
25 |
26 | $path = '/openApi/siteList';
27 | $response = $this->request($path);
28 | $result = json_decode($response, true);
29 | if (isset($result['code']) && $result['code'] == 1000) {
30 | return true;
31 | } else {
32 | throw new Exception(isset($result['message']) ? $result['message'] : '面板地址无法连接');
33 | }
34 | }
35 |
36 | public function deploy($fullchain, $privatekey, $config, &$info)
37 | {
38 | $path = '/openApi/siteList';
39 | $response = $this->request($path);
40 | $result = json_decode($response, true);
41 | if (isset($result['code']) && $result['code'] == 1000) {
42 |
43 | $sites = explode("\n", $config['sites']);
44 | $sites = array_map('trim', $sites);
45 | $success = 0;
46 | $errmsg = null;
47 |
48 | foreach ($result['data'] as $item) {
49 | if (!in_array($item['name'], $sites)) {
50 | continue;
51 | }
52 | try {
53 | $this->deploySite($item['id'], $fullchain, $privatekey);
54 | $this->log("网站 {$item['name']} 证书部署成功");
55 | $success++;
56 | } catch (Exception $e) {
57 | $errmsg = $e->getMessage();
58 | $this->log("网站 {$item['name']} 证书部署失败:" . $errmsg);
59 | }
60 | }
61 | if ($success == 0) {
62 | throw new Exception($errmsg ? $errmsg : '要部署的网站不存在');
63 | }
64 |
65 | } elseif (isset($result['message'])) {
66 | throw new Exception($result['message']);
67 | } else {
68 | throw new Exception($response ? $response : '返回数据解析失败');
69 | }
70 | }
71 |
72 | private function deploySite($id, $fullchain, $privatekey)
73 | {
74 | $path = '/openApi/setSSL';
75 | $data = [
76 | 'id' => $id,
77 | 'key' => $privatekey,
78 | 'pem' => $fullchain,
79 | ];
80 | $response = $this->request($path, $data);
81 | $result = json_decode($response, true);
82 | if (isset($result['code']) && $result['code'] == 1000) {
83 | return true;
84 | } elseif (isset($result['message'])) {
85 | throw new Exception($result['message']);
86 | } else {
87 | throw new Exception($response ? $response : '返回数据解析失败');
88 | }
89 | }
90 |
91 | public function setLogger($func)
92 | {
93 | $this->logger = $func;
94 | }
95 |
96 | private function log($txt)
97 | {
98 | if ($this->logger) {
99 | call_user_func($this->logger, $txt);
100 | }
101 | }
102 |
103 | private function request($path, $params = null)
104 | {
105 | $url = $this->url . $path;
106 |
107 | $headers = [
108 | 'XP-API-KEY' => $this->apikey,
109 | ];
110 | $response = http_request($url, $params ? json_encode($params) : null, null, null, $headers, $this->proxy);
111 | return $response['body'];
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/app/lib/deploy/safeline.php:
--------------------------------------------------------------------------------
1 | url = rtrim($config['url'], '/');
18 | $this->token = $config['token'];
19 | $this->proxy = $config['proxy'] == 1;
20 | }
21 |
22 | public function check()
23 | {
24 | if (empty($this->url) || empty($this->token)) throw new Exception('请填写控制台地址和API Token');
25 | $this->request('/api/open/system');
26 | }
27 |
28 | public function deploy($fullchain, $privatekey, $config, &$info)
29 | {
30 | $domains = $config['domainList'];
31 | if (empty($domains)) throw new Exception('没有设置要部署的域名');
32 |
33 | try {
34 | $data = $this->request('/api/open/cert');
35 | $this->log('获取证书列表成功(total=' . $data['total'] . ')');
36 | } catch (Exception $e) {
37 | throw new Exception('获取证书列表失败:' . $e->getMessage());
38 | }
39 |
40 | $success = 0;
41 | $errmsg = null;
42 | foreach ($data['nodes'] as $row) {
43 | if (empty($row['domains'])) continue;
44 | $flag = false;
45 | foreach ($row['domains'] as $domain) {
46 | if (in_array($domain, $domains) || in_array('*' . substr($domain, strpos($domain, '.')), $domains)) {
47 | $flag = true;
48 | break;
49 | }
50 | }
51 | if ($flag) {
52 | $params = [
53 | 'id' => $row['id'],
54 | 'manual' => [
55 | 'crt' => $fullchain,
56 | 'key' => $privatekey,
57 | ],
58 | 'type' => 2,
59 | ];
60 | try {
61 | $this->request('/api/open/cert', $params);
62 | $this->log("证书ID:{$row['id']}更新成功!");
63 | $success++;
64 | } catch (Exception $e) {
65 | $errmsg = $e->getMessage();
66 | $this->log("证书ID:{$row['id']}更新失败:" . $errmsg);
67 | }
68 | }
69 | }
70 | if ($success == 0) {
71 | $params = [
72 | 'manual' => [
73 | 'crt' => $fullchain,
74 | 'key' => $privatekey,
75 | ],
76 | 'type' => 2,
77 | ];
78 | $this->request('/api/open/cert', $params);
79 | $this->log("证书上传成功!");
80 | }
81 | }
82 |
83 | private function request($path, $params = null)
84 | {
85 | $url = $this->url . $path;
86 | $headers = ['X-SLCE-API-TOKEN' => $this->token];
87 | $body = null;
88 | if ($params) {
89 | $headers['Content-Type'] = 'application/json';
90 | $body = json_encode($params);
91 | }
92 | $response = http_request($url, $body, null, null, $headers, $this->proxy);
93 | $result = json_decode($response['body'], true);
94 | if ($response['code'] == 200 && $result) {
95 | return $result['data'] ?? null;
96 | } else {
97 | throw new Exception(!empty($result['msg']) ? $result['msg'] : '请求失败(httpCode=' . $response['code'] . ')');
98 | }
99 | }
100 |
101 | public function setLogger($func)
102 | {
103 | $this->logger = $func;
104 | }
105 |
106 | private function log($txt)
107 | {
108 | if ($this->logger) {
109 | call_user_func($this->logger, $txt);
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/app/controller/Install.php:
--------------------------------------------------------------------------------
1 | getRootPath() . '.env')) {
16 | return '当前已经安装成功,如果需要重新安装,请手动删除根目录.env文件';
17 | }
18 | if (Request::isPost()) {
19 | $mysql_host = input('post.mysql_host', null, 'trim');
20 | $mysql_port = intval(input('post.mysql_port', '3306'));
21 | $mysql_user = input('post.mysql_user', null, 'trim');
22 | $mysql_pwd = input('post.mysql_pwd', null, 'trim');
23 | $mysql_name = input('post.mysql_name', null, 'trim');
24 | $mysql_prefix = input('post.mysql_prefix', 'cloud_', 'trim');
25 | $admin_username = input('post.admin_username', null, 'trim');
26 | $admin_password = input('post.admin_password', null, 'trim');
27 |
28 | if (!$mysql_host || !$mysql_user || !$mysql_pwd || !$mysql_name || !$admin_username || !$admin_password) {
29 | return json(['code' => 0, 'msg' => '必填项不能为空']);
30 | }
31 |
32 | $configData = file_get_contents(app()->getRootPath() . '.example.env');
33 | $configData = str_replace(['{dbhost}', '{dbname}', '{dbuser}', '{dbpwd}', '{dbport}', '{dbprefix}'], [$mysql_host, $mysql_name, $mysql_user, $mysql_pwd, $mysql_port, $mysql_prefix], $configData);
34 |
35 | try {
36 | $DB = new PDO("mysql:host=" . $mysql_host . ";dbname=" . $mysql_name . ";port=" . $mysql_port, $mysql_user, $mysql_pwd);
37 | } catch (Exception $e) {
38 | if ($e->getCode() == 2002) {
39 | $errorMsg = '连接数据库失败:数据库地址填写错误!';
40 | } elseif ($e->getCode() == 1045) {
41 | $errorMsg = '连接数据库失败:数据库用户名或密码填写错误!';
42 | } elseif ($e->getCode() == 1049) {
43 | $errorMsg = '连接数据库失败:数据库名不存在!';
44 | } else {
45 | $errorMsg = '连接数据库失败:' . $e->getMessage();
46 | }
47 | return json(['code' => 0, 'msg' => $errorMsg]);
48 | }
49 | $DB->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
50 | $DB->exec("set sql_mode = ''");
51 | $DB->exec("set names utf8");
52 |
53 | $sqls = file_get_contents(app()->getAppPath() . 'sql/install.sql');
54 | $sqls = explode(';', $sqls);
55 |
56 | $password = password_hash($admin_password, PASSWORD_DEFAULT);
57 | $sqls[] = "REPLACE INTO `" . $mysql_prefix . "config` VALUES ('sys_key', '" . random(16) . "')";
58 | $sqls[] = "INSERT INTO `" . $mysql_prefix . "user` (`username`,`password`,`level`,`regtime`,`lasttime`,`status`) VALUES ('" . addslashes($admin_username) . "', '$password', 2, NOW(), NOW(), 1)";
59 |
60 | $success = 0;
61 | $error = 0;
62 | $errorMsg = null;
63 | foreach ($sqls as $value) {
64 | $value = trim($value);
65 | if (empty($value)) continue;
66 | $value = str_replace('dnsmgr_', $mysql_prefix, $value);
67 | if ($DB->exec($value) === false) {
68 | $error++;
69 | $dberror = $DB->errorInfo();
70 | $errorMsg .= $dberror[2] . "\n";
71 | } else {
72 | $success++;
73 | }
74 | }
75 | if (empty($errorMsg)) {
76 | if (!file_put_contents(app()->getRootPath() . '.env', $configData)) {
77 | return json(['code' => 0, 'msg' => '保存失败,请确保网站根目录有写入权限']);
78 | }
79 | Cache::clear();
80 | return json(['code' => 1, 'msg' => '安装完成!成功执行SQL语句' . $success . '条']);
81 | } else {
82 | return json(['code' => 0, 'msg' => $errorMsg]);
83 | }
84 | }
85 | return view();
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/app/lib/deploy/lucky.php:
--------------------------------------------------------------------------------
1 | url = rtrim($config['url'], '/') . (!empty($config['path']) ? $config['path'] : '');
18 | $this->opentoken = $config['opentoken'];
19 | $this->proxy = $config['proxy'] == 1;
20 | }
21 |
22 | public function check()
23 | {
24 | if (empty($this->url) || empty($this->opentoken)) throw new Exception('请填写面板地址和OpenToken');
25 | $this->request("/api/modules/list");
26 | }
27 |
28 | public function deploy($fullchain, $privatekey, $config, &$info)
29 | {
30 | $domains = $config['domainList'];
31 | if (empty($domains)) throw new Exception('没有设置要部署的域名');
32 |
33 | try {
34 | $data = $this->request("/api/ssl");
35 | $this->log('获取证书列表成功');
36 | } catch (Exception $e) {
37 | throw new Exception('获取证书列表失败:' . $e->getMessage());
38 | }
39 |
40 | $success = 0;
41 | $errmsg = null;
42 | if (!empty($data['list'])) {
43 | foreach ($data['list'] as $row) {
44 | if (empty($row['CertsInfo']['Domains'])) continue;
45 | $cert_domains = $row['CertsInfo']['Domains'];
46 | $flag = false;
47 | foreach ($cert_domains as $domain) {
48 | if (in_array($domain, $domains)) {
49 | $flag = true;
50 | break;
51 | }
52 | }
53 | if ($flag) {
54 | $params = [
55 | 'Key' => $row['Key'],
56 | 'CertBase64' => base64_encode($fullchain),
57 | 'KeyBase64' => base64_encode($privatekey),
58 | 'AddFrom' => 'file',
59 | 'Enable' => true,
60 | 'MappingToPath' => false,
61 | 'Remark' => $row['Remark'] ?: '',
62 | 'AllSyncClient' => false,
63 | ];
64 | try {
65 | $this->request('/api/ssl', $params, 'PUT');
66 | $this->log("证书ID:{$row['Key']}更新成功!");
67 | $success++;
68 | } catch (Exception $e) {
69 | $errmsg = $e->getMessage();
70 | $this->log("证书ID:{$row['Key']}更新失败:" . $errmsg);
71 | }
72 | }
73 | }
74 | }
75 | if ($success == 0) {
76 | throw new Exception($errmsg ? $errmsg : '没有要更新的证书');
77 | }
78 | }
79 |
80 | public function setLogger($func)
81 | {
82 | $this->logger = $func;
83 | }
84 |
85 | private function log($txt)
86 | {
87 | if ($this->logger) {
88 | call_user_func($this->logger, $txt);
89 | }
90 | }
91 |
92 | private function request($path, $params = null, $method = null)
93 | {
94 | $url = $this->url . $path;
95 |
96 | $headers = [
97 | 'openToken' => $this->opentoken,
98 | ];
99 | $body = null;
100 | if ($params) {
101 | $body = json_encode($params);
102 | $headers['Content-Type'] = 'application/json';
103 | }
104 | $response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
105 | $result = json_decode($response['body'], true);
106 | if (isset($result['ret']) && $result['ret'] == 0) {
107 | return $result;
108 | } elseif (isset($result['msg'])) {
109 | throw new Exception($result['msg']);
110 | } else {
111 | throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/app/service/CertTaskService.php:
--------------------------------------------------------------------------------
1 | execute_deploy()) {
15 | config_set('certdeploy_time', date("Y-m-d H:i:s"));
16 | }
17 | if ($this->execute_order()) {
18 | config_set('certtask_time', date("Y-m-d H:i:s"));
19 | }
20 | }
21 |
22 | private function execute_order()
23 | {
24 | echo '开始执行SSL证书签发任务...'.PHP_EOL;
25 | $days = config_get('cert_renewdays', 7);
26 | $list = Db::name('cert_order')->field('id,aid,status,issend')->whereRaw('status NOT IN (3,4) AND (retrytime IS NULL OR retrytime date('Y-m-d H:i:s', time() + $days * 86400)])->select();
27 | //print_r($list);exit;
28 | $failcount = 0;
29 | foreach ($list as $row) {
30 | if ($row['aid'] == 0) {
31 | if($row['issend'] == 0) MsgNotice::cert_order_send($row['id'], true);
32 | continue;
33 | }
34 | try {
35 | $service = new CertOrderService($row['id']);
36 | if ($row['status'] == 3) {
37 | $service->reset();
38 | }
39 | $retcode = $service->process();
40 | if ($retcode == 3) {
41 | echo 'ID:'.$row['id'].' 证书已签发成功!'.PHP_EOL;
42 | if($row['issend'] == 0) MsgNotice::cert_order_send($row['id'], true);
43 | } elseif ($retcode == 1) {
44 | echo 'ID:'.$row['id'].' 添加DNS记录成功!'.PHP_EOL;
45 | }
46 | break;
47 | } catch (Exception $e) {
48 | echo 'ID:'.$row['id'].' '.$e->getMessage().PHP_EOL;
49 | if ($e->getCode() == 102) {
50 | break;
51 | } elseif ($e->getCode() == 103) {
52 | if($row['issend'] == 0) MsgNotice::cert_order_send($row['id'], false);
53 | } else {
54 | $failcount++;
55 | }
56 | }
57 | if ($failcount >= 3) break;
58 | sleep(1);
59 | }
60 | return true;
61 | }
62 |
63 | private function execute_deploy()
64 | {
65 | $start = config_get('deploy_hour_start', 0);
66 | $end = config_get('deploy_hour_end', 23);
67 | $hour = date('H');
68 | if($start <= $end){
69 | if($hour < $start || $hour > $end){
70 | echo '不在部署任务运行时间范围内'.PHP_EOL; return false;
71 | }
72 | }else{
73 | if($hour < $start && $hour > $end){
74 | echo '不在部署任务运行时间范围内'.PHP_EOL; return false;
75 | }
76 | }
77 |
78 | echo '开始执行SSL证书部署任务...'.PHP_EOL;
79 | $list = Db::name('cert_deploy')->field('id,status,issend')->whereRaw('active=1 AND status IN (0,-1) AND (retrytime IS NULL OR retrytimeselect();
80 | //print_r($list);exit;
81 | $count = 0;
82 | foreach ($list as $row) {
83 | try {
84 | $service = new CertDeployService($row['id']);
85 | $service->process();
86 | echo 'ID:'.$row['id'].' 部署任务执行成功!'.PHP_EOL;
87 | if($row['issend'] == 0) MsgNotice::cert_deploy_send($row['id'], true);
88 | $count++;
89 | } catch (Exception $e) {
90 | echo 'ID:'.$row['id'].' '.$e->getMessage().PHP_EOL;
91 | if ($e->getCode() == 102) {
92 | break;
93 | } elseif ($e->getCode() == 103) {
94 | if($row['issend'] == 0) MsgNotice::cert_deploy_send($row['id'], false);
95 | } else {
96 | $count++;
97 | }
98 | }
99 | if ($count >= 3) break;
100 | sleep(1);
101 | }
102 | return true;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/lib/deploy/lecdn.php:
--------------------------------------------------------------------------------
1 | url = rtrim($config['url'], '/');
22 | $this->email = $config['email'];
23 | $this->password = $config['password'];
24 | $this->auth = isset($config['auth']) ? intval($config['auth']) : 0;
25 | if ($this->auth == 1) {
26 | $this->apiKey = $config['api_key'];
27 | }
28 | $this->proxy = $config['proxy'] == 1;
29 | }
30 |
31 | public function check()
32 | {
33 | if ($this->auth == 1) {
34 | if (empty($this->url) || empty($this->apiKey)) throw new Exception('API访问令牌不能为空');
35 | $this->request('/prod-api/system/info');
36 | } else {
37 | if (empty($this->url) || empty($this->email) || empty($this->password)) throw new Exception('账号和密码不能为空');
38 | $this->login();
39 | }
40 | }
41 |
42 | public function deploy($fullchain, $privatekey, $config, &$info)
43 | {
44 | $id = $config['id'];
45 | if (empty($id)) throw new Exception('证书ID不能为空');
46 |
47 | if ($this->auth == 0) {
48 | $this->login();
49 | }
50 |
51 | try {
52 | $data = $this->request('/prod-api/certificate/' . $id);
53 | } catch (Exception $e) {
54 | throw new Exception('证书ID:' . $id . '获取失败:' . $e->getMessage());
55 | }
56 |
57 | $params = [
58 | 'id' => intval($id),
59 | 'name' => $data['name'],
60 | 'description' => $data['description'],
61 | 'type' => 'upload',
62 | 'ssl_pem' => base64_encode($fullchain),
63 | 'ssl_key' => base64_encode($privatekey),
64 | 'auto_renewal' => false,
65 | ];
66 | $this->request('/prod-api/certificate/' . $id, $params, 'PUT');
67 | $this->log("证书ID:{$id}更新成功!");
68 | }
69 |
70 | private function login()
71 | {
72 | $path = '/prod-api/login';
73 | $params = [
74 | 'email' => $this->email,
75 | 'username' => $this->email,
76 | 'password' => $this->password,
77 | ];
78 | $result = $this->request($path, $params);
79 | if (isset($result['token'])) {
80 | $this->accessToken = $result['token'];
81 | } else {
82 | throw new Exception('登录成功,获取access_token失败');
83 | }
84 | }
85 |
86 | private function request($path, $params = null, $method = null)
87 | {
88 | $url = $this->url . $path;
89 | $headers = [];
90 | $body = null;
91 | if ($this->accessToken) {
92 | $headers['Authorization'] = 'Bearer ' . $this->accessToken;
93 | } elseif ($this->auth == 1 && $this->apiKey) {
94 | $headers['Authorization'] = $this->apiKey;
95 | }
96 | if ($params) {
97 | $headers['Content-Type'] = 'application/json;charset=UTF-8';
98 | $body = json_encode($params);
99 | }
100 | $response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
101 | $result = json_decode($response['body'], true);
102 | if (isset($result['code']) && $result['code'] == 200) {
103 | return $result['data'] ?? null;
104 | } elseif (isset($result['message'])) {
105 | throw new Exception($result['message']);
106 | } else {
107 | throw new Exception('返回数据解析失败');
108 | }
109 | }
110 |
111 | public function setLogger($func)
112 | {
113 | $this->logger = $func;
114 | }
115 |
116 | private function log($txt)
117 | {
118 | if ($this->logger) {
119 | call_user_func($this->logger, $txt);
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/lib/cert/letsencrypt.php:
--------------------------------------------------------------------------------
1 | 'https://acme-v02.api.letsencrypt.org/directory',
13 | 'staging' => 'https://acme-staging-v02.api.letsencrypt.org/directory'
14 | );
15 | private $ac;
16 | private $config;
17 | private $ext;
18 |
19 | public function __construct($config, $ext = null)
20 | {
21 | $this->config = $config;
22 | if (empty($config['mode'])) $config['mode'] = 'live';
23 | $this->ac = new ACMECert($this->directories[$config['mode']], (int)$config['proxy']);
24 | if ($ext) {
25 | $this->ext = $ext;
26 | $this->ac->loadAccountKey($ext['key']);
27 | $this->ac->setAccount($ext['kid']);
28 | }
29 | }
30 |
31 | public function register()
32 | {
33 | if (empty($this->config['email'])) throw new Exception('邮件地址不能为空');
34 |
35 | if (!empty($this->ext['key'])) {
36 | $kid = $this->ac->register(true, $this->config['email']);
37 | return ['kid' => $kid, 'key' => $this->ext['key']];
38 | }
39 |
40 | $key = $this->ac->generateRSAKey(2048);
41 | $this->ac->loadAccountKey($key);
42 | $kid = $this->ac->register(true, $this->config['email']);
43 | return ['kid' => $kid, 'key' => $key];
44 | }
45 |
46 | public function buyCert($domainList, &$order) {}
47 |
48 | public function createOrder($domainList, &$order, $keytype, $keysize)
49 | {
50 | $domain_config = [];
51 | foreach ($domainList as $domain) {
52 | if (empty($domain)) continue;
53 | $domain_config[$domain] = ['challenge' => 'dns-01'];
54 | }
55 | if (empty($domain_config)) throw new Exception('域名列表不能为空');
56 |
57 | $order = $this->ac->createOrder($domain_config);
58 |
59 | $dnsList = [];
60 | if (!empty($order['challenges'])) {
61 | foreach ($order['challenges'] as $opts) {
62 | $mainDomain = getMainDomain($opts['domain']);
63 | $name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
64 | /*if (!array_key_exists($mainDomain, $dnsList)) {
65 | $dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "letsencrypt.org"'];
66 | }*/
67 | $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
68 | }
69 | }
70 |
71 | return $dnsList;
72 | }
73 |
74 | public function authOrder($domainList, $order)
75 | {
76 | $this->ac->authOrder($order);
77 | }
78 |
79 | public function getAuthStatus($domainList, $order)
80 | {
81 | return true;
82 | }
83 |
84 | public function finalizeOrder($domainList, $order, $keytype, $keysize)
85 | {
86 | if (empty($domainList)) throw new Exception('域名列表不能为空');
87 |
88 | if ($keytype == 'ECC') {
89 | if (empty($keysize)) $keysize = '384';
90 | $private_key = $this->ac->generateECKey($keysize);
91 | } else {
92 | if (empty($keysize)) $keysize = '2048';
93 | $private_key = $this->ac->generateRSAKey($keysize);
94 | }
95 | $fullchain = $this->ac->finalizeOrder($domainList, $order, $private_key);
96 |
97 | $certInfo = openssl_x509_parse($fullchain, true);
98 | if (!$certInfo) throw new Exception('证书解析失败');
99 | 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']];
100 | }
101 |
102 | public function revoke($order, $pem)
103 | {
104 | $this->ac->revoke($pem);
105 | }
106 |
107 | public function cancel($order) {}
108 |
109 | public function setLogger($func)
110 | {
111 | $this->ac->setLogger($func);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/public/static/js/respond-1.4.2.min.js:
--------------------------------------------------------------------------------
1 | /*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl
2 | * Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT
3 | * */
4 |
5 | !function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;bconfig = $config;
18 | $this->ac = new ACMECert($config['directory'], (int)$config['proxy']);
19 | if ($ext) {
20 | $this->ext = $ext;
21 | $this->ac->loadAccountKey($ext['key']);
22 | $this->ac->setAccount($ext['kid']);
23 | }
24 | }
25 |
26 | public function register()
27 | {
28 | if (empty($this->config['directory'])) throw new Exception('ACME地址不能为空');
29 | if (empty($this->config['email'])) throw new Exception('邮件地址不能为空');
30 |
31 | if (!empty($this->ext['key'])) {
32 | if (!empty($this->config['kid']) && !empty($this->config['key'])) {
33 | $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']);
34 | } else {
35 | $kid = $this->ac->register(true, $this->config['email']);
36 | }
37 | return ['kid' => $kid, 'key' => $this->ext['key']];
38 | }
39 |
40 | $key = $this->ac->generateRSAKey(2048);
41 | $this->ac->loadAccountKey($key);
42 | if (!empty($this->config['kid']) && !empty($this->config['key'])) {
43 | $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']);
44 | } else {
45 | $kid = $this->ac->register(true, $this->config['email']);
46 | }
47 | return ['kid' => $kid, 'key' => $key];
48 | }
49 |
50 | public function buyCert($domainList, &$order) {}
51 |
52 | public function createOrder($domainList, &$order, $keytype, $keysize)
53 | {
54 | $domain_config = [];
55 | foreach ($domainList as $domain) {
56 | if (empty($domain)) continue;
57 | $domain_config[$domain] = ['challenge' => 'dns-01'];
58 | }
59 | if (empty($domain_config)) throw new Exception('域名列表不能为空');
60 |
61 | $order = $this->ac->createOrder($domain_config);
62 |
63 | $dnsList = [];
64 | if (!empty($order['challenges'])) {
65 | foreach ($order['challenges'] as $opts) {
66 | $mainDomain = getMainDomain($opts['domain']);
67 | $name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
68 | $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
69 | }
70 | }
71 |
72 | return $dnsList;
73 | }
74 |
75 | public function authOrder($domainList, $order)
76 | {
77 | $this->ac->authOrder($order);
78 | }
79 |
80 | public function getAuthStatus($domainList, $order)
81 | {
82 | return true;
83 | }
84 |
85 | public function finalizeOrder($domainList, $order, $keytype, $keysize)
86 | {
87 | if (empty($domainList)) throw new Exception('域名列表不能为空');
88 |
89 | if ($keytype == 'ECC') {
90 | if (empty($keysize)) $keysize = '384';
91 | $private_key = $this->ac->generateECKey($keysize);
92 | } else {
93 | if (empty($keysize)) $keysize = '2048';
94 | $private_key = $this->ac->generateRSAKey($keysize);
95 | }
96 | $fullchain = $this->ac->finalizeOrder($domainList, $order, $private_key);
97 |
98 | $certInfo = openssl_x509_parse($fullchain, true);
99 | if (!$certInfo) throw new Exception('证书解析失败');
100 | 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']];
101 | }
102 |
103 | public function revoke($order, $pem)
104 | {
105 | $this->ac->revoke($pem);
106 | }
107 |
108 | public function cancel($order) {}
109 |
110 | public function setLogger($func)
111 | {
112 | $this->ac->setLogger($func);
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/app/view/system/proxyset.html:
--------------------------------------------------------------------------------
1 | {extend name="common/layout" /}
2 | {block name="title"}代理设置{/block}
3 | {block name="main"}
4 |
5 |
6 |
7 |
代理服务器设置
8 |
42 |
45 |
46 |
47 |
48 | {/block}
49 | {block name="script"}
50 |
51 |
114 | {/block}
--------------------------------------------------------------------------------
/app/lib/deploy/mwpanel.php:
--------------------------------------------------------------------------------
1 | url = rtrim($config['url'], '/');
19 | $this->appid = $config['appid'];
20 | $this->appsecret = $config['appsecret'];
21 | $this->proxy = $config['proxy'] == 1;
22 | }
23 |
24 | public function check()
25 | {
26 | if (empty($this->url) || empty($this->appid) || empty($this->appsecret)) throw new Exception('请填写面板地址和接口密钥');
27 |
28 | $path = '/task/count';
29 | $response = $this->request($path);
30 | $result = json_decode($response, true);
31 | if (isset($result['status']) && $result['status'] == true) {
32 | return true;
33 | } else {
34 | throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接');
35 | }
36 | }
37 |
38 | public function deploy($fullchain, $privatekey, $config, &$info)
39 | {
40 | if ($config['type'] == '1') {
41 | $this->deployPanel($fullchain, $privatekey);
42 | $this->log("面板证书部署成功");
43 | return;
44 | }
45 | $sites = explode("\n", $config['sites']);
46 | $success = 0;
47 | $errmsg = null;
48 | foreach ($sites as $site) {
49 | $siteName = trim($site);
50 | if (empty($siteName)) continue;
51 | try {
52 | $this->deploySite($siteName, $fullchain, $privatekey);
53 | $this->log("网站 {$siteName} 证书部署成功");
54 | $success++;
55 | } catch (Exception $e) {
56 | $errmsg = $e->getMessage();
57 | $this->log("网站 {$siteName} 证书部署失败:" . $errmsg);
58 | }
59 | }
60 | if ($success == 0) {
61 | throw new Exception($errmsg ? $errmsg : '要部署的网站不存在');
62 | }
63 | }
64 |
65 | private function deployPanel($fullchain, $privatekey)
66 | {
67 | $path = '/setting/save_panel_ssl';
68 | $data = [
69 | 'privateKey' => $privatekey,
70 | 'certPem' => $fullchain,
71 | 'choose' => 'local',
72 | ];
73 | $response = $this->request($path, $data);
74 | $result = json_decode($response, true);
75 | if (isset($result['status']) && $result['status']) {
76 | return true;
77 | } elseif (isset($result['msg'])) {
78 | throw new Exception($result['msg']);
79 | } else {
80 | throw new Exception($response ? $response : '返回数据解析失败');
81 | }
82 | }
83 |
84 | private function deploySite($siteName, $fullchain, $privatekey)
85 | {
86 | $path = '/site/set_ssl';
87 | $data = [
88 | 'type' => '1',
89 | 'siteName' => $siteName,
90 | 'key' => $privatekey,
91 | 'csr' => $fullchain,
92 | ];
93 | $response = $this->request($path, $data);
94 | $result = json_decode($response, true);
95 | if (isset($result['status']) && $result['status']) {
96 | return true;
97 | } elseif (isset($result['msg'])) {
98 | throw new Exception($result['msg']);
99 | } else {
100 | throw new Exception($response ? $response : '返回数据解析失败');
101 | }
102 | }
103 |
104 | public function setLogger($func)
105 | {
106 | $this->logger = $func;
107 | }
108 |
109 | private function log($txt)
110 | {
111 | if ($this->logger) {
112 | call_user_func($this->logger, $txt);
113 | }
114 | }
115 |
116 | private function request($path, $params = null)
117 | {
118 | $url = $this->url . $path;
119 |
120 | $headers = [
121 | 'app-id' => $this->appid,
122 | 'app-secret' => $this->appsecret,
123 | ];
124 | $response = http_request($url, $params ? http_build_query($params) : null, null, null, $headers, $this->proxy);
125 | return $response['body'];
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/app/lib/deploy/ftp.php:
--------------------------------------------------------------------------------
1 | config = $config;
16 | }
17 |
18 | public function check()
19 | {
20 | $this->connect();
21 | }
22 |
23 | public function deploy($fullchain, $privatekey, $config, &$info)
24 | {
25 | $conn_id = $this->connect();
26 | ftp_pasv($conn_id, true);
27 | if ($config['format'] == 'pem') {
28 | $temp_stream = fopen('php://temp', 'r+');
29 | fwrite($temp_stream, $fullchain);
30 | rewind($temp_stream);
31 | if (ftp_fput($conn_id, $config['pem_cert_file'], $temp_stream, FTP_BINARY)) {
32 | $this->log('证书文件上传成功:' . $config['pem_cert_file']);
33 | } else {
34 | fclose($temp_stream);
35 | ftp_close($conn_id);
36 | throw new Exception('证书文件上传失败:' . $config['pem_cert_file']);
37 | }
38 | fclose($temp_stream);
39 |
40 | $temp_stream = fopen('php://temp', 'r+');
41 | fwrite($temp_stream, $privatekey);
42 | rewind($temp_stream);
43 | if (ftp_fput($conn_id, $config['pem_key_file'], $temp_stream, FTP_BINARY)) {
44 | $this->log('私钥文件上传成功:' . $config['pem_key_file']);
45 | } else {
46 | fclose($temp_stream);
47 | ftp_close($conn_id);
48 | throw new Exception('私钥文件上传失败:' . $config['pem_key_file']);
49 | }
50 | fclose($temp_stream);
51 | } elseif ($config['format'] == 'pfx') {
52 | $pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null);
53 |
54 | $temp_stream = fopen('php://temp', 'r+');
55 | fwrite($temp_stream, $pfx);
56 | rewind($temp_stream);
57 | if (ftp_fput($conn_id, $config['pfx_file'], $temp_stream, FTP_BINARY)) {
58 | $this->log('PFX证书文件上传成功:' . $config['pfx_file']);
59 | } else {
60 | fclose($temp_stream);
61 | ftp_close($conn_id);
62 | throw new Exception('PFX证书文件上传失败:' . $config['pfx_file']);
63 | }
64 | fclose($temp_stream);
65 | }
66 | ftp_close($conn_id);
67 | }
68 |
69 | private function connect()
70 | {
71 | if (!function_exists('ftp_connect')) {
72 | throw new Exception('ftp扩展未安装');
73 | }
74 | if (empty($this->config['host']) || empty($this->config['port']) || empty($this->config['username']) || empty($this->config['password'])) {
75 | throw new Exception('必填参数不能为空');
76 | }
77 | if (!filter_var($this->config['host'], FILTER_VALIDATE_IP) && !filter_var($this->config['host'], FILTER_VALIDATE_DOMAIN)) {
78 | throw new Exception('主机地址不合法');
79 | }
80 | if (!is_numeric($this->config['port']) || $this->config['port'] < 1 || $this->config['port'] > 65535) {
81 | throw new Exception('端口不合法');
82 | }
83 |
84 | if ($this->config['secure'] == '1') {
85 | $conn_id = ftp_ssl_connect($this->config['host'], intval($this->config['port']), 10);
86 | if (!$conn_id) {
87 | throw new Exception('FTP服务器无法连接(SSL)');
88 | }
89 | } else {
90 | $conn_id = ftp_connect($this->config['host'], intval($this->config['port']), 10);
91 | if (!$conn_id) {
92 | throw new Exception('FTP服务器无法连接');
93 | }
94 | }
95 | if (!ftp_login($conn_id, $this->config['username'], $this->config['password'])) {
96 | ftp_close($conn_id);
97 | throw new Exception('FTP登录失败');
98 | }
99 | return $conn_id;
100 | }
101 |
102 | private function log($txt)
103 | {
104 | if ($this->logger) {
105 | call_user_func($this->logger, $txt);
106 | }
107 | }
108 |
109 | public function setLogger($logger)
110 | {
111 | $this->logger = $logger;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/app/lib/deploy/cdnfly.php:
--------------------------------------------------------------------------------
1 | url = rtrim($config['url'], '/');
22 | $this->api_key = $config['api_key'];
23 | $this->api_secret = $config['api_secret'];
24 | $this->auth = isset($config['auth']) ? $config['auth'] : 0;
25 | if ($this->auth == 1) {
26 | $this->username = $config['username'];
27 | $this->password = $config['password'];
28 | }
29 | $this->proxy = $config['proxy'] == 1;
30 | }
31 |
32 | public function check()
33 | {
34 | if ($this->auth == 1) {
35 | if (empty($this->url) || empty($this->username) || empty($this->password)) throw new Exception('必填参数不能为空');
36 | $this->login();
37 | } else {
38 | if (empty($this->url) || empty($this->api_key) || empty($this->api_secret)) throw new Exception('必填参数不能为空');
39 | $this->request('/v1/user');
40 | }
41 | }
42 |
43 | public function deploy($fullchain, $privatekey, $config, &$info)
44 | {
45 | $id = $config['id'];
46 | if (empty($id)) throw new Exception('证书ID不能为空');
47 |
48 | $params = [
49 | 'type' => 'custom',
50 | 'cert' => $fullchain,
51 | 'key' => $privatekey,
52 | ];
53 | if ($this->auth == 1) {
54 | $access_token = $this->login();
55 | $url = $this->url . '/v1/certs/' . $id;
56 | $body = json_encode($params);
57 | $headers = [
58 | 'Access-Token' => $access_token,
59 | ];
60 | $response = http_request($url, $body, null, null, $headers, $this->proxy, 'PUT');
61 | $result = json_decode($response['body'], true);
62 | if (isset($result['code']) && $result['code'] == 0) {
63 | } elseif (isset($result['msg'])) {
64 | throw new Exception('证书ID:' . $id . '更新失败,' . $result['msg']);
65 | } else {
66 | throw new Exception('证书ID:' . $id . '更新失败,返回数据解析失败');
67 | }
68 | } else {
69 | $this->request('/v1/certs/' . $id, $params, 'PUT');
70 | }
71 | $this->log("证书ID:{$id}更新成功!");
72 | }
73 |
74 | public function login()
75 | {
76 | $url = $this->url . '/v1/login';
77 | $params = [
78 | 'account' => $this->username,
79 | 'password' => $this->password,
80 | ];
81 | $body = json_encode($params);
82 | $response = http_request($url, $body, null, null, null, $this->proxy);
83 | $result = json_decode($response['body'], true);
84 | if (isset($result['code']) && $result['code'] == 0) {
85 | return $result['data']['access_token'];
86 | } elseif (isset($result['msg'])) {
87 | throw new Exception($result['msg']);
88 | } else {
89 | throw new Exception('登录失败,返回数据解析失败');
90 | }
91 | }
92 |
93 | private function request($path, $params = null, $method = null)
94 | {
95 | $url = $this->url . $path;
96 | $headers = ['api-key' => $this->api_key, 'api-secret' => $this->api_secret];
97 | $body = null;
98 | if ($params) {
99 | $headers['Content-Type'] = 'application/json';
100 | $body = json_encode($params);
101 | }
102 | $response = http_request($url, $body, null, null, $headers, $this->proxy, $method);
103 | $result = json_decode($response['body'], true);
104 | if (isset($result['code']) && $result['code'] == 0) {
105 | return isset($result['data']) ? $result['data'] : null;
106 | } elseif (isset($result['msg'])) {
107 | throw new Exception($result['msg']);
108 | } else {
109 | throw new Exception('返回数据解析失败');
110 | }
111 | }
112 |
113 | public function setLogger($func)
114 | {
115 | $this->logger = $func;
116 | }
117 |
118 | private function log($txt)
119 | {
120 | if ($this->logger) {
121 | call_user_func($this->logger, $txt);
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/app/view/optimizeip/opipset.html:
--------------------------------------------------------------------------------
1 | {extend name="common/layout" /}
2 | {block name="title"}CF优选IP设置{/block}
3 | {block name="main"}
4 |
5 |
6 |
7 |
功能简介
8 |
9 |
由于CloudFlare官方IP是泛播路由,同一个IP在不同地区不同运营商所链接的机房是不同的,速度或延迟也会有区别。目前网上也有很多CF优选CNAME服务,然而公共的CNAME可能无法满足稳定性和安全性的需要。
10 |
本功能可以获取CloudFlare最新的优选IP地址(分为电信/联通/移动线路),并自动更新到域名解析记录。
11 |
12 |
13 |
14 |
使用说明
15 |
16 |
不支持对CloudFlare里的域名添加优选,必须使用其他DNS服务商。需开通Cloudflare for SaaS,且域名使用CNAME的方式解析到CloudFlare。
17 |
数据接口:wetest.vip 数据接口支持CloudFlare、CloudFront、EdgeOne;HostMonit 只支持CloudFlare。
18 |
接口密钥:默认o1zrmHAF为免费KEY可永久免费使用。
19 |
自动更新:可查看计划任务设置
20 |
21 |
22 |
23 |
62 |
63 |
64 | {/block}
65 | {block name="script"}
66 |
67 |
121 | {/block}
--------------------------------------------------------------------------------
/app/lib/deploy/doge.php:
--------------------------------------------------------------------------------
1 | AccessKey = $config['AccessKey'];
18 | $this->SecretKey = $config['SecretKey'];
19 | $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
20 | }
21 |
22 | public function check()
23 | {
24 | if (empty($this->AccessKey) || empty($this->SecretKey)) throw new Exception('必填参数不能为空');
25 | $this->request('/cdn/cert/list.json');
26 | return true;
27 | }
28 |
29 | public function deploy($fullchain, $privatekey, $config, &$info)
30 | {
31 | $domains = $config['domain'];
32 | if (empty($domains)) throw new Exception('绑定的域名不能为空');
33 |
34 | $certInfo = openssl_x509_parse($fullchain, true);
35 | if (!$certInfo) throw new Exception('证书解析失败');
36 | $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
37 |
38 | $cert_id = $this->get_cert_id($fullchain, $privatekey, $cert_name);
39 |
40 | foreach (explode(',', $domains) as $domain) {
41 | if (empty($domain)) continue;
42 | $param = [
43 | 'id' => $cert_id,
44 | 'domain' => $domain,
45 | ];
46 | $this->request('/cdn/cert/bind.json', $param);
47 | $this->log('CDN域名 ' . $domain . ' 绑定证书成功!');
48 | }
49 | $info['cert_id'] = $cert_id;
50 | }
51 |
52 | private function get_cert_id($fullchain, $privatekey, $cert_name)
53 | {
54 | $cert_id = null;
55 |
56 | $data = $this->request('/cdn/cert/list.json');
57 | foreach ($data['certs'] as $cert) {
58 | if ($cert_name == $cert['note']) {
59 | $cert_id = $cert['id'];
60 | $this->log('证书' . $cert_name . '已存在,证书ID:' . $cert_id);
61 | } elseif ($cert['expire'] < time() && $cert['domainCount'] == 0) {
62 | try {
63 | $this->request('/cdn/cert/delete.json', ['id' => $cert['id']]);
64 | $this->log('证书' . $cert['name'] . '已过期,删除证书成功');
65 | } catch (Exception $e) {
66 | $this->log('证书' . $cert['name'] . '已过期,删除证书失败:' . $e->getMessage());
67 | }
68 | usleep(300000);
69 | }
70 | }
71 |
72 | if (!$cert_id) {
73 | $param = [
74 | 'note' => $cert_name,
75 | 'cert' => $fullchain,
76 | 'private' => $privatekey,
77 | ];
78 | try {
79 | $data = $this->request('/cdn/cert/upload.json', $param);
80 | } catch (Exception $e) {
81 | throw new Exception('上传证书失败:' . $e->getMessage());
82 | }
83 | $this->log('上传证书成功,证书ID:' . $data['id']);
84 | $cert_id = $data['id'];
85 | usleep(500000);
86 | }
87 | return $cert_id;
88 | }
89 |
90 | private function request($path, $data = null, $json = false)
91 | {
92 | $body = null;
93 | if($data){
94 | $body = $json ? json_encode($data) : http_build_query($data);
95 | }
96 | $signStr = $path . "\n" . $body;
97 | $sign = hash_hmac('sha1', $signStr, $this->SecretKey);
98 | $authorization = "TOKEN " . $this->AccessKey . ":" . $sign;
99 | $headers = ['Authorization' => $authorization];
100 | if($body && $json) $headers['Content-Type'] = 'application/json';
101 | $url = 'https://api.dogecloud.com'.$path;
102 | $response = http_request($url, $body, null, null, $headers, $this->proxy);
103 | $result = json_decode($response['body'], true);
104 | if(isset($result['code']) && $result['code'] == 200){
105 | return $result['data'] ?? true;
106 | }elseif(isset($result['msg'])){
107 | throw new Exception($result['msg']);
108 | }else{
109 | throw new Exception('请求失败');
110 | }
111 | }
112 |
113 | public function setLogger($func)
114 | {
115 | $this->logger = $func;
116 | }
117 |
118 | private function log($txt)
119 | {
120 | if ($this->logger) {
121 | call_user_func($this->logger, $txt);
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/app/lib/deploy/ucloud.php:
--------------------------------------------------------------------------------
1 | PublicKey = $config['PublicKey'];
19 | $this->PrivateKey = $config['PrivateKey'];
20 | $this->client = new UcloudClient($this->PublicKey, $this->PrivateKey);
21 | }
22 |
23 | public function check()
24 | {
25 | if (empty($this->PublicKey) || empty($this->PrivateKey)) throw new Exception('必填参数不能为空');
26 | $param = ['Mode' => 'free'];
27 | $this->client->request('GetCertificateList', $param);
28 | return true;
29 | }
30 |
31 | public function deploy($fullchain, $privatekey, $config, &$info)
32 | {
33 | $domain_id = $config['domain_id'];
34 | if (empty($domain_id)) throw new Exception('云分发资源ID不能为空');
35 |
36 | $certInfo = openssl_x509_parse($fullchain, true);
37 | if (!$certInfo) throw new Exception('证书解析失败');
38 | $cert_name = str_replace(['*', '.'], '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
39 |
40 | $param = [
41 | 'CertName' => $cert_name,
42 | 'UserCert' => $fullchain,
43 | 'PrivateKey' => $privatekey,
44 | ];
45 | try {
46 | $data = $this->client->request('AddCertificate', $param);
47 | $this->log('添加证书成功,名称:' . $cert_name);
48 | } catch (Exception $e) {
49 | if (strpos($e->getMessage(), 'cert already exist') !== false) {
50 | $this->log('证书已存在,名称:' . $cert_name);
51 | } else {
52 | throw new Exception('添加证书失败 ' . $e->getMessage());
53 | }
54 | }
55 |
56 | try {
57 | $data = $this->client->request('GetUcdnDomainConfig', ['DomainId.0' => $domain_id]);
58 | } catch (Exception $e) {
59 | throw new Exception('获取加速域名配置失败 ' . $e->getMessage());
60 | }
61 | if (empty($data['DomainList'])) throw new Exception('云分发资源ID:' . $domain_id . '不存在');
62 | $domain = $data['DomainList'][0]['Domain'];
63 | $HttpsStatusCn = $data['DomainList'][0]['HttpsStatusCn'];
64 | $HttpsStatusAbroad = $data['DomainList'][0]['HttpsStatusAbroad'];
65 |
66 | if ($data['DomainList'][0]['CertNameCn'] == $cert_name || $data['DomainList'][0]['CertNameAbroad'] == $cert_name) {
67 | $this->log('云分发' . $domain_id . '证书已配置,无需重复操作');
68 | return;
69 | }
70 |
71 | try {
72 | $data = $this->client->request('GetCertificateBaseInfoList', ['Domain' => $domain]);
73 | } catch (Exception $e) {
74 | throw new Exception('获取可用证书列表失败 ' . $e->getMessage());
75 | }
76 | if (empty($data['CertList'])) throw new Exception('可用证书列表为空');
77 |
78 | $cert_id = null;
79 | foreach ($data['CertList'] as $cert) {
80 | if ($cert['CertName'] == $cert_name) {
81 | $cert_id = $cert['CertId'];
82 | break;
83 | }
84 | }
85 | if (!$cert_id) throw new Exception('证书ID不存在');
86 | $this->log('证书ID获取成功:' . $cert_id);
87 |
88 | $param = [
89 | 'DomainId' => $domain_id,
90 | 'CertName' => $cert_name,
91 | 'CertId' => $cert_id,
92 | 'CertType' => 'ucdn',
93 | ];
94 | if ($HttpsStatusCn == 'enable') $param['HttpsStatusCn'] = $HttpsStatusCn;
95 | if ($HttpsStatusAbroad == 'enable') $param['HttpsStatusAbroad'] = $HttpsStatusAbroad;
96 | if ($HttpsStatusCn != 'enable' && $HttpsStatusAbroad != 'enable') $param['HttpsStatusCn'] = 'enable';
97 | try {
98 | $data = $this->client->request('UpdateUcdnDomainHttpsConfigV2', $param);
99 | } catch (Exception $e) {
100 | throw new Exception('https加速配置失败 ' . $e->getMessage());
101 | }
102 | $this->log('云分发' . $domain_id . '证书配置成功!');
103 | $info['cert_id'] = $cert_id;
104 | $info['cert_name'] = $cert_name;
105 | }
106 |
107 | public function setLogger($func)
108 | {
109 | $this->logger = $func;
110 | }
111 |
112 | private function log($txt)
113 | {
114 | if ($this->logger) {
115 | call_user_func($this->logger, $txt);
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/app/view/system/cronset.html:
--------------------------------------------------------------------------------
1 | {extend name="common/layout" /}
2 | {block name="title"}计划任务{/block}
3 | {block name="main"}
4 |
5 |
6 |
7 |
计划任务说明
8 |
9 | {if config_get('cron_type', '0') == '1'}
10 |
需定时访问以下URL,频率;1分钟1次
11 |
{$siteurl}/cron?key={:config_get('cron_key')}
12 | {else}
13 |
将以下Shell命令添加到计划任务,频率;1分钟1次
14 |
cd {:app()->getRootPath()} && php think certtask
15 | {if $is_user_www}
注:计划任务执行用户必须选择www用户{/if}
16 |
采用Docker镜像部署的会自动添加计划任务,无需手动添加。
17 | {/if}
18 |
19 |
20 |
21 |
22 |
计划任务设置
23 |
40 |
43 |
44 |
45 |
46 |
计划任务运行状态
47 |
48 |
49 |
50 | | 任务名称 |
51 | 上次运行时间 |
52 |
53 |
54 |
55 |
56 | | SSL证书续签 |
57 | {:config_get('certtask_time', '未运行', true)} |
58 |
59 |
60 | | SSL证书部署 |
61 | {:config_get('certdeploy_time', '未运行', true)} |
62 |
63 |
64 | | 域名到期提醒 |
65 | {:config_get('domain_expire_time', '未运行', true)} |
66 |
67 |
68 | | CF优选IP更新 |
69 | {:config_get('optimize_ip_time', '未运行', true)} |
70 |
71 |
72 | | 定时切换解析 |
73 | {:config_get('schedule_time', '未运行', true)} |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | {/block}
82 | {block name="script"}
83 |
84 |
130 | {/block}
--------------------------------------------------------------------------------
/app/service/ExpireNoticeService.php:
--------------------------------------------------------------------------------
1 | where('id', $id)->update(['regtime' => $regTime, 'expiretime' => $expireTime, 'checktime' => date('Y-m-d H:i:s'), 'checkstatus' => 1]);
20 | return ['code' => 0, 'regTime' => $regTime, 'expireTime' => $expireTime, 'msg' => 'Success'];
21 | } catch (Exception $e) {
22 | Db::name('domain')->where('id', $id)->update(['checktime' => date('Y-m-d H:i:s'), 'checkstatus' => 2]);
23 | return ['code' => -1, 'msg' => $e->getMessage()];
24 | }
25 | }
26 |
27 | public function task()
28 | {
29 | echo '开始执行域名到期提醒任务...' . PHP_EOL;
30 | config_set('domain_expire_time', date("Y-m-d H:i:s"));
31 | $count = $this->refreshDomainList();
32 | if ($count > 0) return;
33 |
34 | $days = config_get('expire_noticedays');
35 | $max_day = 30;
36 | if (!empty($days)) {
37 | $days = explode(',', $days);
38 | $days = array_map('intval', $days);
39 | $max_day = max($days) + 1;
40 | }
41 | $count = $this->refreshExpiringDomainList($max_day);
42 | if ($count > 0) return;
43 |
44 | if (!empty($days) && (config_get('expire_notice_mail') == '1' || config_get('expire_notice_wxtpl') == '1' || config_get('expire_notice_tgbot') == '1' || config_get('expire_notice_webhook') == '1') && date('H') >= 9) {
45 | $this->noticeExpiringDomainList($max_day, $days);
46 | }
47 | }
48 |
49 | private function refreshDomainList()
50 | {
51 | $domainList = Db::name('domain')->field('id,name')->where('checkstatus', 0)->select();
52 | $count = 0;
53 | foreach ($domainList as $domain) {
54 | $res = $this->updateDomainDate($domain['id'], $domain['name']);
55 | if ($res['code'] == 0) {
56 | echo '域名: ' . $domain['name'] . ' 注册时间: ' . $res['regTime'] . ' 到期时间: ' . $res['expireTime'] . PHP_EOL;
57 | } else {
58 | echo '域名: ' . $domain['name'] . ' 更新失败,' . $res['msg'] . PHP_EOL;
59 | }
60 | $count++;
61 | if ($count >= 5) break;
62 | sleep(1);
63 | }
64 | return $count;
65 | }
66 |
67 | private function refreshExpiringDomainList($max_day)
68 | {
69 | $domainList = Db::name('domain')->field('id,name')->whereRaw('expiretime>=(NOW() - INTERVAL 5 DAY) AND expiretime<=(NOW() + INTERVAL ' . $max_day . ' DAY) AND checktime<=(NOW() - INTERVAL 1 DAY)')->select();
70 | $count = 0;
71 | foreach ($domainList as $domain) {
72 | $res = $this->updateDomainDate($domain['id'], $domain['name']);
73 | if ($res['code'] == 0) {
74 | echo '域名: ' . $domain['name'] . ' 注册时间: ' . $res['regTime'] . ' 到期时间: ' . $res['expireTime'] . PHP_EOL;
75 | } else {
76 | echo '域名: ' . $domain['name'] . ' 更新失败,' . $res['msg'] . PHP_EOL;
77 | }
78 | $count++;
79 | if ($count >= 5) break;
80 | sleep(1);
81 | }
82 | return $count;
83 | }
84 |
85 | private function noticeExpiringDomainList($max_day, $days)
86 | {
87 | $domainList = Db::name('domain')->field('id,name,expiretime')->whereRaw('expiretime>=NOW() AND expiretime<=(NOW() + INTERVAL ' . $max_day . ' DAY) AND is_notice=1 AND (noticetime IS NULL OR noticetime<=(NOW() - INTERVAL 20 HOUR))')->order('expiretime', 'asc')->select();
88 | $noticeList = [];
89 | foreach ($domainList as $domain) {
90 | $expireDay = intval((strtotime($domain['expiretime']) - time()) / 86400);
91 | if (in_array($expireDay, $days)) {
92 | $noticeList[$expireDay][] = ['id' => $domain['id'], 'name' => $domain['name'], 'expiretime' => $domain['expiretime']];
93 | }
94 | }
95 | if (!empty($noticeList)) {
96 | foreach ($noticeList as $day => $list) {
97 | $ids = array_column($list, 'id');
98 | Db::name('domain')->whereIn('id', $ids)->update(['noticetime' => date('Y-m-d H:i:s')]);
99 | MsgNotice::expire_notice_send($day, $list);
100 | echo '域名到期提醒: ' . $day . '天内到期的' . count($ids) . '个域名已发送' . PHP_EOL;
101 | }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/public/static/js/moment-2.29.4-locale-zh-cn.js:
--------------------------------------------------------------------------------
1 | //! moment.js locale configuration
2 | //! locale : Chinese (China) [zh-cn]
3 | //! author : suupic : https://github.com/suupic
4 | //! author : Zeno Zeng : https://github.com/zenozeng
5 | //! author : uu109 : https://github.com/uu109
6 |
7 | ;(function (global, factory) {
8 | typeof exports === 'object' && typeof module !== 'undefined'
9 | && typeof require === 'function' ? factory(require('../moment')) :
10 | typeof define === 'function' && define.amd ? define(['../moment'], factory) :
11 | factory(global.moment)
12 | }(this, (function (moment) { 'use strict';
13 |
14 | //! moment.js locale configuration
15 |
16 | var zhCn = moment.defineLocale('zh-cn', {
17 | months: '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split(
18 | '_'
19 | ),
20 | monthsShort: '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split(
21 | '_'
22 | ),
23 | weekdays: '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'),
24 | weekdaysShort: '周日_周一_周二_周三_周四_周五_周六'.split('_'),
25 | weekdaysMin: '日_一_二_三_四_五_六'.split('_'),
26 | longDateFormat: {
27 | LT: 'HH:mm',
28 | LTS: 'HH:mm:ss',
29 | L: 'YYYY/MM/DD',
30 | LL: 'YYYY年M月D日',
31 | LLL: 'YYYY年M月D日Ah点mm分',
32 | LLLL: 'YYYY年M月D日ddddAh点mm分',
33 | l: 'YYYY/M/D',
34 | ll: 'YYYY年M月D日',
35 | lll: 'YYYY年M月D日 HH:mm',
36 | llll: 'YYYY年M月D日dddd HH:mm',
37 | },
38 | meridiemParse: /凌晨|早上|上午|中午|下午|晚上/,
39 | meridiemHour: function (hour, meridiem) {
40 | if (hour === 12) {
41 | hour = 0;
42 | }
43 | if (meridiem === '凌晨' || meridiem === '早上' || meridiem === '上午') {
44 | return hour;
45 | } else if (meridiem === '下午' || meridiem === '晚上') {
46 | return hour + 12;
47 | } else {
48 | // '中午'
49 | return hour >= 11 ? hour : hour + 12;
50 | }
51 | },
52 | meridiem: function (hour, minute, isLower) {
53 | var hm = hour * 100 + minute;
54 | if (hm < 600) {
55 | return '凌晨';
56 | } else if (hm < 900) {
57 | return '早上';
58 | } else if (hm < 1130) {
59 | return '上午';
60 | } else if (hm < 1230) {
61 | return '中午';
62 | } else if (hm < 1800) {
63 | return '下午';
64 | } else {
65 | return '晚上';
66 | }
67 | },
68 | calendar: {
69 | sameDay: '[今天]LT',
70 | nextDay: '[明天]LT',
71 | nextWeek: function (now) {
72 | if (now.week() !== this.week()) {
73 | return '[下]dddLT';
74 | } else {
75 | return '[本]dddLT';
76 | }
77 | },
78 | lastDay: '[昨天]LT',
79 | lastWeek: function (now) {
80 | if (this.week() !== now.week()) {
81 | return '[上]dddLT';
82 | } else {
83 | return '[本]dddLT';
84 | }
85 | },
86 | sameElse: 'L',
87 | },
88 | dayOfMonthOrdinalParse: /\d{1,2}(日|月|周)/,
89 | ordinal: function (number, period) {
90 | switch (period) {
91 | case 'd':
92 | case 'D':
93 | case 'DDD':
94 | return number + '日';
95 | case 'M':
96 | return number + '月';
97 | case 'w':
98 | case 'W':
99 | return number + '周';
100 | default:
101 | return number;
102 | }
103 | },
104 | relativeTime: {
105 | future: '%s后',
106 | past: '%s前',
107 | s: '几秒',
108 | ss: '%d 秒',
109 | m: '1 分钟',
110 | mm: '%d 分钟',
111 | h: '1 小时',
112 | hh: '%d 小时',
113 | d: '1 天',
114 | dd: '%d 天',
115 | w: '1 周',
116 | ww: '%d 周',
117 | M: '1 个月',
118 | MM: '%d 个月',
119 | y: '1 年',
120 | yy: '%d 年',
121 | },
122 | week: {
123 | // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效
124 | dow: 1, // Monday is the first day of the week.
125 | doy: 4, // The week that contains Jan 4th is the first week of the year.
126 | },
127 | });
128 |
129 | return zhCn;
130 |
131 | })));
132 |
--------------------------------------------------------------------------------
/app/lib/deploy/west.php:
--------------------------------------------------------------------------------
1 | username = $config['username'];
19 | $this->api_password = $config['api_password'];
20 | $this->proxy = $config['proxy'] == 1;
21 | }
22 |
23 | public function check()
24 | {
25 | if (empty($this->username) || empty($this->api_password)) throw new Exception('用户名或API密码不能为空');
26 | $this->execute('/vhost/', ['act' => 'products']);
27 | }
28 |
29 | public function deploy($fullchain, $privatekey, $config, &$info)
30 | {
31 | if (empty($config['sitename'])) throw new Exception('FTP账号不能为空');
32 | $params = [
33 | 'act' => 'vhostssl',
34 | 'sitename' => $config['sitename'],
35 | 'cmd' => 'info'
36 | ];
37 | try {
38 | $data = $this->execute('/vhost/', $params);
39 | } catch (Exception $e) {
40 | throw new Exception('获取虚拟主机SSL配置失败:' . $e->getMessage());
41 | }
42 |
43 | $params = [
44 | 'act' => 'vhostssl',
45 | 'sitename' => $config['sitename'],
46 | 'cmd' => 'import',
47 | 'keycontent' => $privatekey,
48 | 'certcontent' => $fullchain,
49 | ];
50 | try {
51 | $this->execute('/vhost/', $params);
52 | } catch (Exception $e) {
53 | throw new Exception('上传SSL证书失败:' . $e->getMessage());
54 | }
55 | $this->log('SSL证书上传成功');
56 |
57 | if (!isset($data['SSLEnabled']) || $data['SSLEnabled'] == 0) {
58 | $params = [
59 | 'act' => 'vhostssl',
60 | 'sitename' => $config['sitename'],
61 | 'cmd' => 'openssl',
62 | ];
63 | try {
64 | $this->execute('/vhost/', $params);
65 | } catch (Exception $e) {
66 | throw new Exception('虚拟主机部署SSL失败:' . $e->getMessage());
67 | }
68 | } else {
69 | $params = [
70 | 'act' => 'vhostssl',
71 | 'sitename' => $config['sitename'],
72 | 'cmd' => 'info'
73 | ];
74 | try {
75 | $data = $this->execute('/vhost/', $params);
76 | } catch (Exception $e) {
77 | throw new Exception('获取虚拟主机SSL配置失败:' . $e->getMessage());
78 | }
79 | if (!empty($data['sslcert']['ssl'])) {
80 | foreach ($data['sslcert']['ssl'] as $domain => $row) {
81 | if (!in_array($domain, $config['domainList'])) continue;
82 | $params = [
83 | 'act' => 'vhostssl',
84 | 'sitename' => $config['sitename'],
85 | 'cmd' => 'clearsslcache',
86 | 'sslid' => $row['sysid'],
87 | 'dm' => $domain,
88 | ];
89 | try {
90 | $this->execute('/vhost/', $params);
91 | $this->log('更新' . $domain . '证书缓存成功');
92 | } catch (Exception $e) {
93 | $this->log('更新' . $domain . '证书缓存失败:' . $e->getMessage());
94 | }
95 | }
96 | }
97 | }
98 | $this->log('虚拟主机' . $config['sitename'] . '部署SSL成功');
99 | }
100 |
101 | private function execute($path, $params)
102 | {
103 | $params['username'] = $this->username;
104 | $params['time'] = getMillisecond();
105 | $params['token'] = md5($this->username . $this->api_password . $params['time']);
106 | $response = http_request($this->baseUrl . $path, str_replace('+', '%20', http_build_query($params)), null, null, null, $this->proxy);
107 | $response = mb_convert_encoding($response['body'], 'UTF-8', 'GBK');
108 | $arr = json_decode($response, true);
109 | if ($arr) {
110 | if ($arr['result'] == 200) {
111 | return isset($arr['data']) ? $arr['data'] : [];
112 | } else {
113 | throw new Exception($arr['msg']);
114 | }
115 | } else {
116 | throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
117 | }
118 | }
119 |
120 | public function setLogger($func)
121 | {
122 | $this->logger = $func;
123 | }
124 |
125 | private function log($txt)
126 | {
127 | if ($this->logger) {
128 | call_user_func($this->logger, $txt);
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------