├── 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 |
6 |
7 |
8 |
9 | 10 | 11 |
12 |
13 |
14 |
15 |
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 |
6 |
7 |

登录验证码设置

8 |
9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 |
17 |
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 |
6 |
7 |
8 |
9 | 10 |
11 |
12 | 13 | {if request()->user['level'] eq 2}{/if} 14 | 15 | 16 |
17 | 18 | 刷新 19 |
20 | 21 | 22 |
23 |
24 |
25 |
26 |
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 |
8 |
9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 | 刷新 21 |   24H告警次数:{$info.fail_count}  切换次数:{$info.switch_count} 22 |
23 | 24 | 25 |
26 |
27 |
28 |
29 |
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 | 3 | 4 | Group 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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 |
5 |
6 |
7 |
8 | 9 |
10 |
11 | 12 | 13 |
14 | 15 | 刷新 16 | 添加 17 |
18 | 19 | 20 |
21 |
22 |
23 |
24 |
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 |
5 |
6 |
7 |
8 | 9 |
10 |
11 | 12 | 13 |
14 | 15 | 刷新 16 | 添加 17 |
18 | 19 | 20 |
21 |
22 |
23 |
24 |
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?"
':"")+'
"+l+'
'+n.content+"
"+c+"
",!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 | 3 | 4 | Group 3 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /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 |
5 |
6 | 7 |
8 |

返回域名到期提醒设置

9 |
10 |
11 |
12 | 13 |
域名到期前多少天发送通知,可填写多个天数,用英文逗号隔开。例如填写7,14则在域名到期前7天与14天分别发送通知。
14 |
15 |
16 | 17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 |
38 | 41 |
42 | 43 |
44 |
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 |
9 |
10 |
11 | 12 |
13 |

14 |
15 | 16 |
17 |

18 |
19 | 20 |
21 |

22 |
23 | 24 |
25 |

26 |
27 | 28 |
35 |

36 |
37 | 39 |
40 |
41 |
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 |
    24 |
    25 |

    数据接口设置

    26 |
    27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 | 34 |
    35 |
    36 |
    37 |
    38 | 39 | 查询积分 40 |
    41 |
    42 |
    43 |
    44 |
    45 |
    46 |

    自动更新设置

    47 |
    48 |
    49 |
    50 | 51 |
    52 |
    53 |
    54 |
    55 | 56 |
    57 |
    58 |
    59 |
    60 |
    61 |
    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 |
    24 |
    25 |
    26 | 27 |
    28 |
    29 |
    30 | 31 |
    32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 |
    40 | 43 |
    44 | 45 |
    46 |

    计划任务运行状态

    47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
    任务名称上次运行时间
    SSL证书续签{:config_get('certtask_time', '未运行', true)}
    SSL证书部署{:config_get('certdeploy_time', '未运行', true)}
    域名到期提醒{:config_get('domain_expire_time', '未运行', true)}
    CF优选IP更新{:config_get('optimize_ip_time', '未运行', true)}
    定时切换解析{:config_get('schedule_time', '未运行', true)}
    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 | --------------------------------------------------------------------------------