diff --git a/README.md b/README.md index fec5066..17116a6 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ - 提供API接口,可获取域名单独的登录链接,方便各种IDC系统对接 - 容灾切换功能,支持ping、tcp、http(s)检测协议并自动暂停/修改域名解析,并支持邮件、微信公众号通知 - CF优选IP功能,支持获取最新的Cloudflare优选IP,并自动更新到解析记录 +- SSL证书申请与自动部署功能,支持从Let's Encrypt等渠道申请SSL证书,并自动部署到各种面板、云服务商、服务器等 ### 演示截图 @@ -38,6 +39,18 @@ CF优选IP功能,添加优选IP任务 ![](https://p1.meituan.net/csc/da70c76753aee4bce044d16fadd56e5f217660.png) +SSL证书申请功能 + +![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154857.png) + +![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154652.png) + +SSL证书自动部署功能 + +![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154702.png) + +![](https://blog.cccyun.cn/content/uploadfile/202412/QQ%E6%88%AA%E5%9B%BE20241221154804.png) + ### 部署方法 * 从[Release](https://github.com/netcccyun/dnsmgr/releases)页面下载安装包 @@ -158,9 +171,9 @@ create database dnsmgr; 在install界面链接IP填写dnsmgr-mysql -### 版权信息 +### 作者信息 -版权所有Copyright © 2023~2024 by 消失的彩虹海(https://blog.cccyun.cn) +消失的彩虹海(https://blog.cccyun.cn) ### 其他推荐 diff --git a/app/command/Certtask.php b/app/command/Certtask.php new file mode 100644 index 0000000..443e5a3 --- /dev/null +++ b/app/command/Certtask.php @@ -0,0 +1,33 @@ +setName('certtask') + ->setDescription('证书申请与部署任务'); + } + + protected function execute(Input $input, Output $output) + { + $res = Db::name('config')->cache('configs', 0)->column('value', 'key'); + Config::set($res, 'sys'); + + (new CertTaskService())->execute(); + } +} diff --git a/app/command/Dmtask.php b/app/command/Dmtask.php index 3df1c3b..f69eb53 100644 --- a/app/command/Dmtask.php +++ b/app/command/Dmtask.php @@ -12,7 +12,7 @@ use think\console\input\Option; use think\console\Output; use think\facade\Db; use think\facade\Config; -use app\lib\TaskRunner; +use app\service\TaskRunner; class Dmtask extends Command { diff --git a/app/command/Opiptask.php b/app/command/Opiptask.php index 030cf1f..df60bed 100644 --- a/app/command/Opiptask.php +++ b/app/command/Opiptask.php @@ -12,7 +12,7 @@ use think\console\input\Option; use think\console\Output; use think\facade\Db; use think\facade\Config; -use app\lib\OptimizeService; +use app\service\OptimizeService; class Opiptask extends Command { diff --git a/app/command/Reset.php b/app/command/Reset.php new file mode 100644 index 0000000..019387f --- /dev/null +++ b/app/command/Reset.php @@ -0,0 +1,47 @@ +setName('reset') + ->addArgument('type', Argument::REQUIRED, '操作类型,pwd:重置密码,totp:关闭TOTP') + ->addArgument('username', Argument::REQUIRED, '用户名') + ->addArgument('password', Argument::OPTIONAL, '密码') + ->setDescription('重置密码'); + } + + protected function execute(Input $input, Output $output) + { + $type = trim($input->getArgument('type')); + $username = trim($input->getArgument('username')); + $user = Db::name('user')->where('username', $username)->find(); + if (!$user) { + $output->writeln('用户 ' . $username . ' 不存在'); + return; + } + if ($type == 'pwd') { + $password = $input->getArgument('password'); + if (empty($password)) $password = '123456'; + Db::name('user')->where('id', $user['id'])->update(['password' => password_hash($password, PASSWORD_DEFAULT)]); + $output->writeln('用户 ' . $username . ' 密码重置成功'); + } elseif ($type == 'totp') { + Db::name('user')->where('id', $user['id'])->update(['totp_open' => 0, 'totp_secret' => null]); + $output->writeln('用户 ' . $username . ' TOTP关闭成功'); + } + } +} diff --git a/app/controller/Auth.php b/app/controller/Auth.php index e6581c4..7d7891e 100644 --- a/app/controller/Auth.php +++ b/app/controller/Auth.php @@ -3,6 +3,7 @@ namespace app\controller; use app\BaseController; +use Exception; use think\facade\Db; class Auth extends BaseController @@ -37,12 +38,14 @@ class Auth extends BaseController $user = Db::name('user')->where('username', $username)->find(); if ($user && password_verify($password, $user['password'])) { if ($user['status'] == 0) return json(['code' => -1, 'msg' => '此用户已被封禁', 'vcode' => 1]); - Db::name('log')->insert(['uid' => $user['id'], 'action' => '登录后台', 'data' => 'IP:' . $this->clientip, 'addtime' => date("Y-m-d H:i:s")]); - DB::name('user')->where('id', $user['id'])->update(['lasttime' => date("Y-m-d H:i:s")]); - $session = md5($user['id'] . $user['password']); - $expiretime = time() + 2562000; - $token = authcode("user\t{$user['id']}\t{$session}\t{$expiretime}", 'ENCODE', config_get('sys_key')); - cookie('user_token', $token, ['expire' => $expiretime, 'httponly' => true]); + if ($user['totp_open'] == 1 && !empty($user['totp_secret'])) { + session('pre_login_user', $user['id']); + if (file_exists($login_limit_file)) { + unlink($login_limit_file); + } + return json(['code' => -1, 'msg' => '需要验证动态口令', 'vcode' => 2]); + } + $this->loginUser($user); if (file_exists($login_limit_file)) { unlink($login_limit_file); } @@ -50,6 +53,7 @@ class Auth extends BaseController } else { if ($user) { Db::name('log')->insert(['uid' => $user['id'], 'action' => '登录失败', 'data' => 'IP:' . $this->clientip, 'addtime' => date("Y-m-d H:i:s")]); + if ($user['totp_open'] == 1 && !empty($user['totp_secret'])) $login_limit_count = 10; } if (!file_exists($login_limit_file)) { $login_limit = ['count' => 0, 'time' => 0]; @@ -69,6 +73,28 @@ class Auth extends BaseController return view(); } + public function totp() + { + $uid = session('pre_login_user'); + if (empty($uid)) return json(['code' => -1, 'msg' => '请重新登录']); + $code = input('post.code'); + if (empty($code)) return json(['code' => -1, 'msg' => '请输入动态口令']); + $user = Db::name('user')->where('id', $uid)->find(); + if (!$user) return json(['code' => -1, 'msg' => '用户不存在']); + if ($user['totp_open'] == 0 || empty($user['totp_secret'])) return json(['code' => -1, 'msg' => '未开启TOTP二次验证']); + try { + $totp = \app\lib\TOTP::create($user['totp_secret']); + if (!$totp->verify($code)) { + return json(['code' => -1, 'msg' => '动态口令错误']); + } + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + $this->loginUser($user); + session('pre_login_user', null); + return json(['code' => 0]); + } + public function logout() { cookie('user_token', null); @@ -101,13 +127,27 @@ class Auth extends BaseController return $this->alert('error', '该域名不支持快捷登录'); } - Db::name('log')->insert(['uid' => 0, 'action' => '域名快捷登录', 'data' => 'IP:' . $this->clientip, 'addtime' => date("Y-m-d H:i:s"), 'domain' => $domain]); + $this->loginDomain($row); + return redirect('/record/' . $row['id']); + } + private function loginUser($user) + { + Db::name('log')->insert(['uid' => $user['id'], 'action' => '登录后台', 'data' => 'IP:' . $this->clientip, 'addtime' => date("Y-m-d H:i:s")]); + DB::name('user')->where('id', $user['id'])->update(['lasttime' => date("Y-m-d H:i:s")]); + $session = md5($user['id'] . $user['password']); + $expiretime = time() + 2562000; + $token = authcode("user\t{$user['id']}\t{$session}\t{$expiretime}", 'ENCODE', config_get('sys_key')); + cookie('user_token', $token, ['expire' => $expiretime, 'httponly' => true]); + } + + private function loginDomain($row) + { + Db::name('log')->insert(['uid' => 0, 'action' => '域名快捷登录', 'data' => 'IP:' . $this->clientip, 'addtime' => date("Y-m-d H:i:s"), 'domain' => $row['name']]); $session = md5($row['id'] . $row['name']); $expiretime = time() + 2562000; $token = authcode("domain\t{$row['id']}\t{$session}\t{$expiretime}", 'ENCODE', config_get('sys_key')); cookie('user_token', $token, ['expire' => $expiretime, 'httponly' => true]); - return redirect('/record/' . $row['id']); } public function verifycode() diff --git a/app/controller/Cert.php b/app/controller/Cert.php new file mode 100644 index 0000000..36bec3f --- /dev/null +++ b/app/controller/Cert.php @@ -0,0 +1,735 @@ +alert('error', '无权限'); + return view(); + } + + public function deployaccount() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + return view(); + } + + public function account_data() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $deploy = input('get.deploy/d', 0); + $kw = $this->request->post('kw', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('cert_account')->where('deploy', $deploy); + if (!empty($kw)) { + $select->whereLike('name|remark', '%' . $kw . '%'); + } + $total = $select->count(); + $rows = $select->order('id', 'desc')->limit($offset, $limit)->select(); + + $list = []; + foreach ($rows as $row) { + $row['typename'] = $deploy == 1 ? DeployHelper::$deploy_config[$row['type']]['name'] : CertHelper::$cert_config[$row['type']]['name']; + $row['icon'] = $deploy == 1 ? DeployHelper::$deploy_config[$row['type']]['icon'] : CertHelper::$cert_config[$row['type']]['icon']; + $list[] = $row; + } + + return json(['total' => $total, 'rows' => $list]); + } + + public function account_op() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + $deploy = input('post.deploy/d', 0); + $title = $deploy == 1 ? '自动部署账户' : 'SSL证书账户'; + + if ($action == 'add') { + $type = input('post.type'); + $name = input('post.name', null, 'trim'); + $config = input('post.config', null, 'trim'); + $remark = input('post.remark', null, 'trim'); + if ($type == 'local') $name = '复制到本机'; + if (empty($name) || empty($config)) return json(['code' => -1, 'msg' => '必填参数不能为空']); + if (Db::name('cert_account')->where('type', $type)->where('config', $config)->find()) { + return json(['code' => -1, 'msg' => $title.'已存在']); + } + Db::startTrans(); + $id = Db::name('cert_account')->insertGetId([ + 'type' => $type, + 'name' => $name, + 'config' => $config, + 'remark' => $remark, + 'deploy' => $deploy, + 'addtime' => date('Y-m-d H:i:s'), + ]); + try { + $this->checkAccount($id, $type, $deploy); + Db::commit(); + return json(['code' => 0, 'msg' => '添加'.$title.'成功!']); + } catch(Exception $e) { + Db::rollback(); + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } elseif ($action == 'edit') { + $id = input('post.id/d'); + $row = Db::name('cert_account')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => $title.'不存在']); + $type = input('post.type'); + $name = input('post.name', null, 'trim'); + $config = input('post.config', null, 'trim'); + $remark = input('post.remark', null, 'trim'); + if ($type == 'local') $name = '复制到本机'; + if (empty($name) || empty($config)) return json(['code' => -1, 'msg' => '必填参数不能为空']); + if (Db::name('cert_account')->where('type', $type)->where('config', $config)->where('id', '<>', $id)->find()) { + return json(['code' => -1, 'msg' => $title.'已存在']); + } + Db::startTrans(); + Db::name('cert_account')->where('id', $id)->update([ + 'type' => $type, + 'name' => $name, + 'config' => $config, + 'remark' => $remark, + ]); + try { + $this->checkAccount($id, $type, $deploy); + Db::commit(); + return json(['code' => 0, 'msg' => '修改'.$title.'成功!']); + } catch(Exception $e) { + Db::rollback(); + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } elseif ($action == 'del') { + $id = input('post.id/d'); + if($deploy == 0){ + $dcount = DB::name('cert_order')->where('aid', $id)->count(); + if ($dcount > 0) return json(['code' => -1, 'msg' => '该'.$title.'下存在证书订单,无法删除']); + }else{ + $dcount = DB::name('cert_deploy')->where('aid', $id)->count(); + if ($dcount > 0) return json(['code' => -1, 'msg' => '该'.$title.'下存在自动部署任务,无法删除']); + } + Db::name('cert_account')->where('id', $id)->delete(); + return json(['code' => 0]); + } + return json(['code' => -3]); + } + + public function account_form() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + $deploy = input('get.deploy/d', 0); + $title = $deploy == 1 ? '自动部署账户' : 'SSL证书账户'; + + $account = null; + if ($action == 'edit') { + $id = input('get.id/d'); + $account = Db::name('cert_account')->where('id', $id)->find(); + if (empty($account)) return $this->alert('error', $title.'不存在'); + } + + $typeList = $deploy == 1 ? DeployHelper::getList() : CertHelper::getList(); + $classList = $deploy == 1 ? DeployHelper::$class_config : CertHelper::$class_config; + + View::assign('title', $title); + View::assign('info', $account); + View::assign('typeList', $typeList); + View::assign('classList', $classList); + View::assign('action', $action); + View::assign('deploy', $deploy); + return View::fetch(); + } + + private function checkAccount($id, $type, $deploy) + { + if($deploy == 0){ + $mod = CertHelper::getModel($id); + if($mod){ + try{ + $ext = $mod->register(); + if(is_array($ext)){ + Db::name('cert_account')->where('id', $id)->update(['ext'=>json_encode($ext)]); + } + return true; + }catch(Exception $e){ + throw new Exception('验证SSL证书账户失败,' . $e->getMessage()); + } + }else{ + throw new Exception('SSL证书申请模块'.$type.'不存在'); + } + }else{ + $mod = DeployHelper::getModel($id); + if($mod){ + try{ + $mod->check(); + return true; + }catch(Exception $e){ + throw new Exception('验证自动部署账户失败,' . $e->getMessage()); + } + }else{ + throw new Exception('SSL证书申请模块'.$type.'不存在'); + } + } + } + + public function certorder() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $types = []; + foreach(CertHelper::$cert_config as $key=>$value){ + $types[$key] = $value['name']; + } + View::assign('types', $types); + return view(); + } + + public function order_data() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $domain = $this->request->post('domain', null, 'trim'); + $id = input('post.id'); + $type = input('post.type', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('cert_order')->alias('A')->join('cert_account B', 'A.aid = B.id'); + if (!empty($id)) { + $select->where('A.id', $id); + }elseif (!empty($domain)) { + $oids = Db::name('cert_domain')->where('domain', 'like', '%' . $domain . '%')->column('oid'); + $select->whereIn('A.id', $oids); + } + if (!empty($type)) { + $select->where('B.type', $type); + } + $total = $select->count(); + $rows = $select->fieldRaw('A.*,B.type,B.remark aremark')->order('id', 'desc')->limit($offset, $limit)->select(); + + $list = []; + foreach ($rows as $row) { + $row['typename'] = CertHelper::$cert_config[$row['type']]['name']; + $row['icon'] = CertHelper::$cert_config[$row['type']]['icon']; + $row['domains'] = Db::name('cert_domain')->where('oid', $row['id'])->order('sort','ASC')->column('domain'); + $row['end_day'] = $row['expiretime'] ? ceil((strtotime($row['expiretime']) - time()) / 86400) : null; + if($row['error']) $row['error'] = htmlspecialchars(str_replace("'", "\\'", $row['error'])); + $list[] = $row; + } + + return json(['total' => $total, 'rows' => $list]); + } + + public function order_op() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + + if ($action == 'get') { + $id = input('post.id/d'); + $row = Db::name('cert_order')->where('id', $id)->field('fullchain,privatekey')->find(); + if (!$row) return $this->alert('error', '证书订单不存在'); + $pfx = CertHelper::getPfx($row['fullchain'], $row['privatekey']); + $row['pfx'] = base64_encode($pfx); + return json(['code' => 0, 'data' => $row]); + } elseif ($action == 'add') { + $domains = input('post.domains', [], 'trim'); + $order = [ + 'aid' => input('post.aid/d'), + 'keytype' => input('post.keytype'), + 'keysize' => input('post.keysize'), + 'addtime' => date('Y-m-d H:i:s'), + 'status' => 0, + ]; + $domains = array_map('trim', $domains); + $domains = array_filter($domains, function ($v) { + return !empty($v); + }); + $domains = array_unique($domains); + if (empty($domains)) return json(['code' => -1, 'msg' => '绑定域名不能为空']); + if (empty($order['aid']) || empty($order['keytype']) || empty($order['keysize'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + + $res = $this->check_order($order, $domains); + if (is_array($res)) return json($res); + + Db::startTrans(); + $id = Db::name('cert_order')->insertGetId($order); + $domainList = []; + $i=1; + foreach($domains as $domain){ + $domainList[] = [ + 'oid' => $id, + 'domain' => $domain, + 'sort' => $i++, + ]; + } + Db::name('cert_domain')->insertAll($domainList); + Db::commit(); + return json(['code' => 0, 'msg' => '添加证书订单成功!']); + } elseif ($action == 'edit') { + $id = input('post.id/d'); + $row = Db::name('cert_order')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => '证书订单不存在']); + + $domains = input('post.domains', [], 'trim'); + $order = [ + 'aid' => input('post.aid/d'), + 'keytype' => input('post.keytype'), + 'keysize' => input('post.keysize'), + 'updatetime' => date('Y-m-d H:i:s'), + ]; + $domains = array_map('trim', $domains); + $domains = array_filter($domains, function ($v) { + return !empty($v); + }); + $domains = array_unique($domains); + if (empty($domains)) return json(['code' => -1, 'msg' => '绑定域名不能为空']); + if (empty($order['aid']) || empty($order['keytype']) || empty($order['keysize'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + + $res = $this->check_order($order, $domains); + if (is_array($res)) return json($res); + + Db::startTrans(); + Db::name('cert_order')->where('id', $id)->update($order); + Db::name('cert_domain')->where('oid', $id)->delete(); + $domainList = []; + $i=1; + foreach($domains as $domain){ + $domainList[] = [ + 'oid' => $id, + 'domain' => $domain, + 'sort' => $i++, + ]; + } + Db::name('cert_domain')->insertAll($domainList); + Db::commit(); + return json(['code' => 0, 'msg' => '修改证书订单成功!']); + } elseif ($action == 'del') { + $id = input('post.id/d'); + $dcount = DB::name('cert_deploy')->where('oid', $id)->count(); + if ($dcount > 0) return json(['code' => -1, 'msg' => '该证书关联了自动部署任务,无法删除']); + try{ + (new CertOrderService($id))->cancel(); + }catch(Exception $e){ + } + Db::name('cert_order')->where('id', $id)->delete(); + Db::name('cert_domain')->where('oid', $id)->delete(); + return json(['code' => 0]); + } elseif ($action == 'setauto') { + $id = input('post.id/d'); + $isauto = input('post.isauto/d'); + Db::name('cert_order')->where('id', $id)->update(['isauto' => $isauto]); + return json(['code' => 0]); + } elseif ($action == 'reset') { + $id = input('post.id/d'); + try{ + $service = new CertOrderService($id); + $service->cancel(); + $service->reset(); + return json(['code' => 0]); + }catch(Exception $e){ + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } elseif ($action == 'revoke') { + $id = input('post.id/d'); + try{ + $service = new CertOrderService($id); + $service->revoke(); + return json(['code' => 0]); + }catch(Exception $e){ + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } elseif ($action == 'show_log') { + $processid = input('post.processid'); + $file = app()->getRuntimePath().'log/'.$processid.'.log'; + if(!file_exists($file)) return json(['code' => -1, 'msg' => '日志文件不存在']); + return json(['code' => 0, 'data' => file_get_contents($file), 'time'=>filemtime($file)]); + } + return json(['code' => -3]); + } + + private function check_order($order, $domains) + { + $account = Db::name('cert_account')->where('id', $order['aid'])->find(); + if (!$account) return ['code' => -1, 'msg' => 'SSL证书账户不存在']; + $max_domains = CertHelper::$cert_config[$account['type']]['max_domains']; + $wildcard = CertHelper::$cert_config[$account['type']]['wildcard']; + $cname = CertHelper::$cert_config[$account['type']]['cname']; + if (count($domains) > $max_domains) return ['code' => -1, 'msg' => '域名数量不能超过'.$max_domains.'个']; + + foreach($domains as $domain){ + if(!$wildcard && strpos($domain, '*') !== false) return ['code' => -1, 'msg' => '该证书账户类型不支持泛域名']; + $mainDomain = getMainDomain($domain); + $drow = Db::name('domain')->where('name', $mainDomain)->find(); + if (!$drow) { + if (!$cname || !Db::name('cert_cname')->where('domain', $domain)->where('status', 1)->find()) { + return ['code' => -1, 'msg' => '域名'.$domain.'未在本系统添加']; + } + } + } + return true; + } + + public function order_process() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + if (function_exists("set_time_limit")) { + @set_time_limit(0); + } + if (function_exists("ignore_user_abort")) { + @ignore_user_abort(true); + } + $id = input('post.id/d'); + $reset = input('post.reset/d', 0); + try{ + $service = new CertOrderService($id); + if($reset == 1){ + $service->reset(); + } + $retcode = $service->process(true); + if($retcode == 3){ + return json(['code' => 0, 'msg' => '证书已签发成功!']); + }elseif($retcode == 1){ + return json(['code' => 0, 'msg' => '添加DNS记录成功!请等待DNS生效后点击验证']); + } + }catch(Exception $e){ + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } + + public function order_form() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + + $order = null; + if ($action == 'edit') { + $id = input('get.id/d'); + $order = Db::name('cert_order')->where('id', $id)->fieldRaw('id,aid,keytype,keysize,status')->find(); + if (empty($order)) return $this->alert('error', '证书订单不存在'); + $order['domains'] = Db::name('cert_domain')->where('oid', $order['id'])->order('sort','ASC')->column('domain'); + } + + $accounts = []; + foreach (Db::name('cert_account')->where('deploy', 0)->select() as $row) { + $accounts[$row['id']] = ['name'=>$row['id'].'_'.CertHelper::$cert_config[$row['type']]['name'], 'type'=>$row['type']]; + if (!empty($row['remark'])) { + $accounts[$row['id']]['name'] .= '(' . $row['remark'] . ')'; + } + } + View::assign('accounts', $accounts); + + View::assign('info', $order); + View::assign('action', $action); + return View::fetch(); + } + + + public function deploytask() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $types = []; + foreach(DeployHelper::$deploy_config as $key=>$value){ + $types[$key] = $value['name']; + } + View::assign('types', $types); + return view(); + } + + public function deploy_data() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $domain = $this->request->post('domain', null, 'trim'); + $oid = input('post.oid'); + $type = input('post.type', null, 'trim'); + $remark = input('post.remark', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('cert_deploy')->alias('A')->join('cert_account B', 'A.aid = B.id')->join('cert_order C', 'A.oid = C.id')->join('cert_account D', 'C.aid = D.id'); + if (!empty($oid)) { + $select->where('A.oid', $oid); + } elseif (!empty($domain)) { + $oids = Db::name('cert_domain')->where('domain', 'like', '%' . $domain . '%')->column('oid'); + $select->whereIn('oid', $oids); + } + if (!empty($type)) { + $select->where('B.type', $type); + } + if (!empty($remark)) { + $select->where('A.remark', $remark); + } + $total = $select->count(); + $rows = $select->fieldRaw('A.*,B.type,B.remark aremark,B.name aname,D.type certtype,D.id certaid')->order('id', 'desc')->limit($offset, $limit)->select(); + + $list = []; + foreach ($rows as $row) { + $row['typename'] = DeployHelper::$deploy_config[$row['type']]['name']; + $row['icon'] = DeployHelper::$deploy_config[$row['type']]['icon']; + $row['certtypename'] = CertHelper::$cert_config[$row['certtype']]['name']; + $row['domains'] = Db::name('cert_domain')->where('oid', $row['oid'])->order('sort','ASC')->column('domain'); + if($row['error']) $row['error'] = htmlspecialchars(str_replace("'", "\\'", $row['error'])); + $list[] = $row; + } + + return json(['total' => $total, 'rows' => $list]); + } + + public function deploy_op() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + + if ($action == 'add') { + $task = [ + 'aid' => input('post.aid/d'), + 'oid' => input('post.oid/d'), + 'config' => input('post.config', null, 'trim'), + 'remark' => input('post.remark', null, 'trim'), + 'addtime' => date('Y-m-d H:i:s'), + 'status' => 0, + 'active' => 1 + ]; + if (empty($task['aid']) || empty($task['oid']) || empty($task['config'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + Db::name('cert_deploy')->insert($task); + return json(['code' => 0, 'msg' => '添加自动部署任务成功!']); + } elseif ($action == 'edit') { + $id = input('post.id/d'); + $row = Db::name('cert_deploy')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => '自动部署任务不存在']); + + $task = [ + 'aid' => input('post.aid/d'), + 'oid' => input('post.oid/d'), + 'config' => input('post.config', null, 'trim'), + 'remark' => input('post.remark', null, 'trim'), + ]; + if (empty($task['aid']) || empty($task['oid']) || empty($task['config'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + Db::name('cert_deploy')->where('id', $id)->update($task); + return json(['code' => 0, 'msg' => '修改自动部署任务成功!']); + } elseif ($action == 'del') { + $id = input('post.id/d'); + Db::name('cert_deploy')->where('id', $id)->delete(); + return json(['code' => 0]); + } elseif ($action == 'setactive') { + $id = input('post.id/d'); + $active = input('post.active/d'); + Db::name('cert_deploy')->where('id', $id)->update(['active' => $active]); + return json(['code' => 0]); + } elseif ($action == 'reset') { + $id = input('post.id/d'); + try{ + $service = new CertDeployService($id); + $service->reset(); + return json(['code' => 0]); + }catch(Exception $e){ + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } elseif ($action == 'show_log') { + $processid = input('post.processid'); + $file = app()->getRuntimePath().'log/'.$processid.'.log'; + if(!file_exists($file)) return json(['code' => -1, 'msg' => '日志文件不存在']); + return json(['code' => 0, 'data' => file_get_contents($file), 'time'=>filemtime($file)]); + } + return json(['code' => -3]); + } + + public function deploy_process() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + if (function_exists("set_time_limit")) { + @set_time_limit(0); + } + if (function_exists("ignore_user_abort")) { + @ignore_user_abort(true); + } + $id = input('post.id/d'); + $reset = input('post.reset/d', 0); + try{ + $service = new CertDeployService($id); + if($reset == 1){ + $service->reset(); + } + $service->process(true); + return json(['code' => 0, 'msg' => 'SSL证书部署任务执行成功!']); + }catch(Exception $e){ + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } + + public function deploy_form() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + + $task = null; + if ($action == 'edit') { + $id = input('get.id/d'); + $task = Db::name('cert_deploy')->alias('A')->join('cert_account B', 'A.aid = B.id')->where('A.id', $id)->fieldRaw('A.id,A.aid,A.oid,A.config,A.remark,B.type')->find(); + if (empty($task)) return $this->alert('error', '自动部署任务不存在'); + } + + $accounts = []; + foreach (Db::name('cert_account')->where('deploy', 1)->select() as $row) { + $accounts[$row['id']] = ['name'=>$row['id'].'_'.DeployHelper::$deploy_config[$row['type']]['name'], 'type'=>$row['type']]; + if (!empty($row['remark'])) { + $accounts[$row['id']]['name'] .= '(' . $row['remark'] . ')'; + } + } + View::assign('accounts', $accounts); + + $orders = []; + foreach (Db::name('cert_order')->alias('A')->join('cert_account B', 'A.aid = B.id')->where('status', '<>', 4)->fieldRaw('A.id,A.aid,B.type,B.remark aremark')->order('id', 'desc')->select() as $row) { + $domains = Db::name('cert_domain')->where('oid', $row['id'])->order('sort','ASC')->column('domain'); + $domainstr = count($domains) > 2 ? implode('、',array_slice($domains, 0, 2)).'等'.count($domains).'个域名' : implode('、',$domains); + $orders[$row['id']] = ['name'=>$row['id'].'_'.$domainstr.'('.CertHelper::$cert_config[$row['type']]['name'].')']; + } + View::assign('orders', $orders); + + View::assign('info', $task); + View::assign('action', $action); + View::assign('typeList', DeployHelper::getList()); + return View::fetch(); + } + + public function cname() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $domains = []; + foreach (Db::name('domain')->field('id,name')->select() as $row) { + $domains[$row['id']] = $row['name']; + } + View::assign('domains', $domains); + return view(); + } + + public function cname_data() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $kw = $this->request->post('kw', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id'); + if (!empty($kw)) { + $select->whereLike('A.domain', '%' . $kw . '%'); + } + $total = $select->count(); + $rows = $select->order('A.id', 'desc')->limit($offset, $limit)->field('A.*,B.name cnamedomain')->select(); + + $list = []; + foreach ($rows as $row) { + $row['host'] = $this->getCnameHost($row['domain']); + $row['record'] = $row['rr'] . '.' . $row['cnamedomain']; + $list[] = $row; + } + + return json(['total' => $total, 'rows' => $list]); + } + + private function getCnameHost($domain) + { + $main = getMainDomain($domain); + if ($main == $domain) { + return '_acme-challenge'; + } else { + return '_acme-challenge.' . substr($domain, 0, -strlen($main) - 1); + } + } + + public function cname_op() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + + if ($action == 'add') { + $data = [ + 'domain' => input('post.domain', null, 'trim'), + 'rr' => input('post.rr', null, 'trim'), + 'did' => input('post.did/d'), + 'addtime' => date('Y-m-d H:i:s'), + 'status' => 0 + ]; + if (empty($data['domain']) || empty($data['rr']) || empty($data['did'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + if (!checkDomain($data['domain'])) return json(['code' => -1, 'msg' => '域名格式不正确']); + if (Db::name('cert_cname')->where('domain', $data['domain'])->find()) { + return json(['code' => -1, 'msg' => '域名'.$data['domain'].'已存在']); + } + if (Db::name('cert_cname')->where('rr', $data['rr'])->where('did', $data['did'])->find()) { + return json(['code' => -1, 'msg' => '已存在相同CNAME记录值']); + } + Db::name('cert_cname')->insert($data); + return json(['code' => 0, 'msg' => '添加CMAME代理成功!']); + } elseif ($action == 'edit') { + $id = input('post.id/d'); + $row = Db::name('cert_cname')->where('id', $id)->find(); + if (!$row) return json(['code' => -1, 'msg' => 'CMAME代理不存在']); + + $data = [ + 'rr' => input('post.rr', null, 'trim'), + 'did' => input('post.did/d'), + ]; + if ($row['rr'] != $data['rr'] || $row['did'] != $data['did']) { + $data['status'] = 0; + } + if (empty($data['rr']) || empty($data['did'])) return json(['code' => -1, 'msg' => '必填参数不能为空']); + if (Db::name('cert_cname')->where('rr', $data['rr'])->where('did', $data['did'])->where('id', '<>', $id)->find()) { + return json(['code' => -1, 'msg' => '已存在相同CNAME记录值']); + } + Db::name('cert_cname')->where('id', $id)->update($data); + return json(['code' => 0, 'msg' => '修改CMAME代理成功!']); + } elseif ($action == 'del') { + $id = input('post.id/d'); + Db::name('cert_cname')->where('id', $id)->delete(); + return json(['code' => 0]); + } elseif ($action == 'check') { + $id = input('post.id/d'); + $row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.id', $id)->field('A.*,B.name cnamedomain')->find(); + if (!$row) return json(['code' => -1, 'msg' => '自动部署任务不存在']); + + $status = 1; + $domain = '_acme-challenge.' . $row['domain']; + $record = $row['rr'] . '.' . $row['cnamedomain']; + $result = \app\utils\DnsQueryUtils::get_dns_records($domain, 'CNAME'); + if(!$result || !in_array($record, $result)){ + $result = \app\utils\DnsQueryUtils::query_dns_doh($domain, 'CNAME'); + if(!$result || !in_array($record, $result)){ + $status = 0; + } + } + if($status != $row['status']){ + Db::name('cert_cname')->where('id', $id)->update(['status' => $status]); + } + return json(['code' => 0, 'status' => $status]); + } + } + + public function certset() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + if ($this->request->isPost()) { + $params = input('post.'); + foreach ($params as $key => $value) { + if (empty($key)) { + continue; + } + config_set($key, $value); + Cache::delete('configs'); + } + return json(['code' => 0, 'msg' => 'succ']); + } + return View::fetch(); + } +} diff --git a/app/controller/Dmonitor.php b/app/controller/Dmonitor.php index 0ed4683..f62120f 100644 --- a/app/controller/Dmonitor.php +++ b/app/controller/Dmonitor.php @@ -3,7 +3,6 @@ namespace app\controller; use app\BaseController; -use Exception; use think\facade\Db; use think\facade\View; use think\facade\Cache; @@ -219,87 +218,15 @@ class Dmonitor extends BaseController public function noticeset() { if (!checkPermission(2)) return $this->alert('error', '无权限'); - if ($this->request->isPost()) { - $params = input('post.'); - if (isset($params['mail_type']) && isset($params['mail_name2']) && $params['mail_type'] > 0) { - $params['mail_name'] = $params['mail_name2']; - unset($params['mail_name2']); + $params = input('post.'); + foreach ($params as $key => $value) { + if (empty($key)) { + continue; } - foreach ($params as $key => $value) { - if (empty($key)) { - continue; - } - config_set($key, $value); - Cache::delete('configs'); - } - return json(['code' => 0, 'msg' => 'succ']); + config_set($key, $value); + Cache::delete('configs'); } - return View::fetch(); - } - - public function proxyset() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - if ($this->request->isPost()) { - $params = input('post.'); - foreach ($params as $key => $value) { - if (empty($key)) { - continue; - } - config_set($key, $value); - Cache::delete('configs'); - } - return json(['code' => 0, 'msg' => 'succ']); - } - return View::fetch(); - } - - public function mailtest() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); - if (empty($mail_name)) return json(['code' => -1, 'msg' => '您还未设置邮箱!']); - $result = \app\lib\MsgNotice::send_mail($mail_name, '邮件发送测试。', '这是一封测试邮件!

来自:' . $this->request->root(true)); - if ($result === true) { - return json(['code' => 0, 'msg' => '邮件发送成功!']); - } else { - return json(['code' => -1, 'msg' => '邮件发送失败!' . $result]); - } - } - - public function tgbottest() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $tgbot_token = config_get('tgbot_token'); - $tgbot_chatid = config_get('tgbot_chatid'); - if (empty($tgbot_token) || empty($tgbot_chatid)) return json(['code' => -1, 'msg' => '请先保存设置']); - $content = "消息发送测试\n\n这是一封测试消息!\n\n来自:" . $this->request->root(true); - $result = \app\lib\MsgNotice::send_telegram_bot($content); - if ($result === true) { - return json(['code' => 0, 'msg' => '消息发送成功!']); - } else { - return json(['code' => -1, 'msg' => '消息发送失败!' . $result]); - } - } - - public function proxytest() - { - if (!checkPermission(2)) return $this->alert('error', '无权限'); - $proxy_server = trim($_POST['proxy_server']); - $proxy_port = $_POST['proxy_port']; - $proxy_user = trim($_POST['proxy_user']); - $proxy_pwd = trim($_POST['proxy_pwd']); - $proxy_type = $_POST['proxy_type']; - try { - check_proxy('https://dl.amh.sh/ip.htm', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd); - } catch (Exception $e) { - try { - check_proxy('https://myip.ipip.net/', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd); - } catch (Exception $e) { - return json(['code' => -1, 'msg' => $e->getMessage()]); - } - } - return json(['code' => 0]); + return json(['code' => 0, 'msg' => 'succ']); } public function clean() diff --git a/app/controller/Index.php b/app/controller/Index.php index 6a2467e..ea99692 100644 --- a/app/controller/Index.php +++ b/app/controller/Index.php @@ -8,6 +8,7 @@ use think\facade\Db; use think\facade\View; use think\facade\Cache; use app\lib\DnsHelper; +use app\utils\MsgNotice; class Index extends BaseController { @@ -120,16 +121,46 @@ class Index extends BaseController Db::name('user')->where('id', $this->request->user['id'])->update(['password' => password_hash($newpwd, PASSWORD_DEFAULT)]); return json(['code' => 0, 'msg' => 'succ']); } + View::assign('user', $this->request->user); return view(); } + public function totp() + { + if (!checkPermission(1)) return $this->alert('error', '无权限'); + $action = input('param.action'); + if ($action == 'generate') { + try { + $totp = \app\lib\TOTP::create(); + $totp->setLabel($this->request->user['username']); + $totp->setIssuer('DNS Manager'); + return json(['code' => 0, 'data' => ['secret' => $totp->getSecret(), 'qrcode' => $totp->getProvisioningUri()]]); + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } elseif ($action == 'bind') { + $secret = input('post.secret'); + $code = input('post.code'); + if (empty($secret)) return json(['code' => -1, 'msg' => '密钥不能为空']); + if (empty($code)) return json(['code' => -1, 'msg' => '请输入动态口令']); + try { + $totp = \app\lib\TOTP::create($secret); + if (!$totp->verify($code)) { + return json(['code' => -1, 'msg' => '动态口令错误']); + } + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + Db::name('user')->where('id', $this->request->user['id'])->update(['totp_open' => 1, 'totp_secret' => $secret]); + return json(['code' => 0, 'msg' => 'succ']); + } elseif ($action == 'close') { + Db::name('user')->where('id', $this->request->user['id'])->update(['totp_open' => 0, 'totp_secret' => null]); + return json(['code' => 0, 'msg' => 'succ']); + } + } + public function test() { - //$a = \app\lib\DnsQueryUtils::query_dns_doh('www.cccyun.cc', 'A'); - //print_r($a); - $dnsList = json_decode('{"cccyun.net":[{"name":"@","type":"CAA","value":"0 issue \"letsencrypt.org\""},{"name":"verify","type":"TXT","value":"TXTTEST1"},{"name":"verify","type":"TXT","value":"TXTTEST2"}],"yuncname.com":[{"name":"@","type":"CAA","value":"0 issue \"letsencrypt.org\""},{"name":"verify.testhost1","type":"CNAME","value":"i.trust.com"},{"name":"verify.testhost2","type":"CNAME","value":"i.trust.com"}]}', true); - \app\lib\CertDnsUtils::addDns($dnsList, function ($txt) { - echo $txt . PHP_EOL; - }); + } } diff --git a/app/controller/Optimizeip.php b/app/controller/Optimizeip.php index 61a5941..bf76857 100644 --- a/app/controller/Optimizeip.php +++ b/app/controller/Optimizeip.php @@ -7,7 +7,7 @@ use Exception; use think\facade\Db; use think\facade\View; use think\facade\Cache; -use app\lib\OptimizeService; +use app\service\OptimizeService; class Optimizeip extends BaseController { diff --git a/app/controller/System.php b/app/controller/System.php new file mode 100644 index 0000000..9411a14 --- /dev/null +++ b/app/controller/System.php @@ -0,0 +1,98 @@ +alert('error', '无权限'); + if ($this->request->isPost()) { + $params = input('post.'); + if (isset($params['mail_type']) && isset($params['mail_name2']) && $params['mail_type'] > 0) { + $params['mail_name'] = $params['mail_name2']; + unset($params['mail_name2']); + } + foreach ($params as $key => $value) { + if (empty($key)) { + continue; + } + config_set($key, $value); + Cache::delete('configs'); + } + return json(['code' => 0, 'msg' => 'succ']); + } + return View::fetch(); + } + + public function proxyset() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + if ($this->request->isPost()) { + $params = input('post.'); + foreach ($params as $key => $value) { + if (empty($key)) { + continue; + } + config_set($key, $value); + Cache::delete('configs'); + } + return json(['code' => 0, 'msg' => 'succ']); + } + return View::fetch(); + } + + public function mailtest() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); + if (empty($mail_name)) return json(['code' => -1, 'msg' => '您还未设置邮箱!']); + $result = \app\utils\MsgNotice::send_mail($mail_name, '邮件发送测试。', '这是一封测试邮件!

来自:' . $this->request->root(true)); + if ($result === true) { + return json(['code' => 0, 'msg' => '邮件发送成功!']); + } else { + return json(['code' => -1, 'msg' => '邮件发送失败!' . $result]); + } + } + + public function tgbottest() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $tgbot_token = config_get('tgbot_token'); + $tgbot_chatid = config_get('tgbot_chatid'); + if (empty($tgbot_token) || empty($tgbot_chatid)) return json(['code' => -1, 'msg' => '请先保存设置']); + $content = "消息发送测试\n\n这是一封测试消息!\n\n来自:" . $this->request->root(true); + $result = \app\utils\MsgNotice::send_telegram_bot($content); + if ($result === true) { + return json(['code' => 0, 'msg' => '消息发送成功!']); + } else { + return json(['code' => -1, 'msg' => '消息发送失败!' . $result]); + } + } + + public function proxytest() + { + if (!checkPermission(2)) return $this->alert('error', '无权限'); + $proxy_server = trim($_POST['proxy_server']); + $proxy_port = $_POST['proxy_port']; + $proxy_user = trim($_POST['proxy_user']); + $proxy_pwd = trim($_POST['proxy_pwd']); + $proxy_type = $_POST['proxy_type']; + try { + check_proxy('https://dl.amh.sh/ip.htm', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd); + } catch (Exception $e) { + try { + check_proxy('https://myip.ipip.net/', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd); + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } + return json(['code' => 0]); + } +} \ No newline at end of file diff --git a/app/data/domain_root.txt b/app/data/domain_root.txt new file mode 100644 index 0000000..991d7d4 --- /dev/null +++ b/app/data/domain_root.txt @@ -0,0 +1,1925 @@ +ac.ae +co.ae +net.ae +org.ae +sch.ae +airport.aero +cargo.aero +charter.aero +com.af +edu.af +gov.af +net.af +org.af +co.ag +com.ag +net.ag +nom.ag +org.ag +com.ai +net.ai +off.ai +org.ai +com.al +net.al +org.al +co.am +com.am +net.am +north.am +org.am +radio.am +south.am +co.ao +it.ao +og.ao +pb.ao +com.ar +int.ar +net.ar +org.ar +co.at +or.at +asn.au +com.au +id.au +info.au +net.au +org.au +biz.az +co.az +com.az +edu.az +gov.az +info.az +int.az +mil.az +name.az +net.az +org.az +pp.az +pro.az +co.ba +co.bb +com.bb +net.bb +org.bb +ac.bd +com.bd +net.bd +org.bd +com.bh +co.bi +com.bi +edu.bi +info.bi +mo.bi +net.bi +or.bi +org.bi +auz.biz +com.bj +edu.bj +com.bm +net.bm +org.bm +com.bn +com.bo +net.bo +org.bo +tv.bo +abc.br +adm.br +adv.br +agr.br +am.br +aparecida.br +arq.br +art.br +ato.br +belem.br +bhz.br +bio.br +blog.br +bmd.br +boavista.br +bsb.br +campinas.br +caxias.br +cim.br +cng.br +cnt.br +com.br +coop.br +curitiba.br +ecn.br +eco.br +emp.br +eng.br +esp.br +etc.br +eti.br +far.br +flog.br +floripa.br +fm.br +fnd.br +fortal.br +fot.br +foz.br +fst.br +g12.br +ggf.br +gru.br +imb.br +ind.br +inf.br +jampa.br +jor.br +lel.br +macapa.br +maceio.br +manaus.br +mat.br +med.br +mil.br +mus.br +natal.br +net.br +nom.br +not.br +ntr.br +odo.br +org.br +palmas.br +poa.br +ppg.br +pro.br +psc.br +psi.br +qsl.br +radio.br +rec.br +recife.br +rep.br +rio.br +salvador.br +sjc.br +slg.br +srv.br +taxi.br +teo.br +tmp.br +trd.br +tur.br +tv.br +vet.br +vix.br +vlog.br +wiki.br +zlg.br +com.bs +net.bs +org.bs +com.bt +org.bt +ac.bw +co.bw +net.bw +org.bw +com.by +minsk.by +net.by +co.bz +com.bz +net.bz +org.bz +za.bz +com.cd +net.cd +org.cd +ac.ci +co.ci +com.ci +ed.ci +edu.ci +go.ci +in.ci +int.ci +net.ci +nom.ci +or.ci +org.ci +biz.ck +co.ck +edu.ck +gen.ck +gov.ck +info.ck +net.ck +org.ck +co.cm +com.cm +net.cm +ac.cn +ah.cn +bj.cn +com.cn +cq.cn +fj.cn +gd.cn +gs.cn +gx.cn +gz.cn +ha.cn +hb.cn +he.cn +hi.cn +hk.cn +hl.cn +hn.cn +jl.cn +js.cn +jx.cn +ln.cn +mo.cn +net.cn +nm.cn +nx.cn +org.cn +qh.cn +sc.cn +sd.cn +sh.cn +sn.cn +sx.cn +tj.cn +tw.cn +xj.cn +xz.cn +yn.cn +zj.cn +com.co +net.co +nom.co +ae.com +africa.com +ar.com +br.com +cn.com +co.com +de.com +eu.com +gb.com +gr.com +hk.com +hu.com +jpn.com +kr.com +mex.com +no.com +nv.com +pty-ltd.com +qc.com +ru.com +sa.com +se.com +uk.com +us.com +uy.com +za.com +co.cr +ed.cr +fi.cr +go.cr +or.cr +sa.cr +com.cu +com.cv +int.cv +net.cv +nome.cv +org.cv +publ.cv +com.cw +net.cw +ac.cy +biz.cy +com.cy +ekloges.cy +ltd.cy +name.cy +net.cy +org.cy +parliament.cy +press.cy +pro.cy +tm.cy +co.cz +co.de +com.de +biz.dk +co.dk +co.dm +com.dm +net.dm +org.dm +art.do +com.do +net.do +org.do +sld.do +web.do +com.dz +com.ec +fin.ec +info.ec +med.ec +net.ec +org.ec +pro.ec +co.ee +com.ee +fie.ee +med.ee +pri.ee +com.eg +edu.eg +eun.eg +gov.eg +info.eg +name.eg +net.eg +org.eg +tv.eg +com.es +edu.es +gob.es +nom.es +org.es +biz.et +com.et +info.et +name.et +net.et +org.et +biz.fj +com.fj +info.fj +name.fj +net.fj +org.fj +pro.fj +co.fk +radio.fm +aeroport.fr +asso.fr +avocat.fr +chambagri.fr +chirurgiens-dentistes.fr +com.fr +experts-comptables.fr +geometre-expert.fr +gouv.fr +medecin.fr +nom.fr +notaires.fr +pharmacien.fr +port.fr +prd.fr +presse.fr +tm.fr +veterinaire.fr +com.ge +edu.ge +gov.ge +mil.ge +net.ge +org.ge +pvt.ge +co.gg +net.gg +org.gg +com.gh +edu.gh +org.gh +com.gi +gov.gi +ltd.gi +org.gi +co.gl +com.gl +edu.gl +net.gl +org.gl +com.gn +gov.gn +net.gn +org.gn +com.gp +mobi.gp +net.gp +org.gp +com.gr +edu.gr +net.gr +org.gr +com.gt +ind.gt +net.gt +org.gt +com.gu +co.gy +com.gy +net.gy +com.hk +edu.hk +gov.hk +idv.hk +inc.hk +ltd.hk +net.hk +org.hk +公司.hk +com.hn +edu.hn +net.hn +org.hn +com.hr +adult.ht +art.ht +asso.ht +com.ht +edu.ht +firm.ht +info.ht +net.ht +org.ht +perso.ht +pol.ht +pro.ht +rel.ht +shop.ht +2000.hu +agrar.hu +bolt.hu +casino.hu +city.hu +co.hu +erotica.hu +erotika.hu +film.hu +forum.hu +games.hu +hotel.hu +info.hu +ingatlan.hu +jogasz.hu +konyvelo.hu +lakas.hu +media.hu +news.hu +org.hu +priv.hu +reklam.hu +sex.hu +shop.hu +sport.hu +suli.hu +szex.hu +tm.hu +tozsde.hu +utazas.hu +video.hu +biz.id +co.id +my.id +or.id +web.id +co.il +net.il +org.il +ac.im +co.im +com.im +ltd.co.im +net.im +org.im +plc.co.im +co.in +firm.in +gen.in +ind.in +net.in +org.in +auz.info +com.iq +co.ir +abr.it +abruzzo.it +ag.it +agrigento.it +al.it +alessandria.it +alto-adige.it +altoadige.it +an.it +ancona.it +andria-barletta-trani.it +andria-trani-barletta.it +andriabarlettatrani.it +andriatranibarletta.it +ao.it +aosta.it +aoste.it +ap.it +aq.it +aquila.it +ar.it +arezzo.it +ascoli-piceno.it +ascolipiceno.it +asti.it +at.it +av.it +avellino.it +ba.it +balsan.it +bari.it +barletta-trani-andria.it +barlettatraniandria.it +bas.it +basilicata.it +belluno.it +benevento.it +bergamo.it +bg.it +bi.it +biella.it +bl.it +bn.it +bo.it +bologna.it +bolzano.it +bozen.it +br.it +brescia.it +brindisi.it +bs.it +bt.it +bz.it +ca.it +cagliari.it +cal.it +calabria.it +caltanissetta.it +cam.it +campania.it +campidano-medio.it +campidanomedio.it +campobasso.it +carbonia-iglesias.it +carboniaiglesias.it +carrara-massa.it +carraramassa.it +caserta.it +catania.it +catanzaro.it +cb.it +ce.it +cesena-forli.it +cesenaforli.it +ch.it +chieti.it +ci.it +cl.it +cn.it +co.it +como.it +cosenza.it +cr.it +cremona.it +crotone.it +cs.it +ct.it +cuneo.it +cz.it +dell-ogliastra.it +dellogliastra.it +emilia-romagna.it +emiliaromagna.it +emr.it +en.it +enna.it +fc.it +fe.it +fermo.it +ferrara.it +fg.it +fi.it +firenze.it +florence.it +fm.it +foggia.it +forli-cesena.it +forlicesena.it +fr.it +friuli-v-giulia.it +friuli-ve-giulia.it +friuli-vegiulia.it +friuli-venezia-giulia.it +friuli-veneziagiulia.it +friuli-vgiulia.it +friuliv-giulia.it +friulive-giulia.it +friulivegiulia.it +friulivenezia-giulia.it +friuliveneziagiulia.it +friulivgiulia.it +frosinone.it +fvg.it +ge.it +genoa.it +genova.it +go.it +gorizia.it +gr.it +grosseto.it +iglesias-carbonia.it +iglesiascarbonia.it +im.it +imperia.it +is.it +isernia.it +kr.it +la-spezia.it +laquila.it +laspezia.it +latina.it +laz.it +lazio.it +lc.it +le.it +lecce.it +lecco.it +li.it +lig.it +liguria.it +livorno.it +lo.it +lodi.it +lom.it +lombardia.it +lombardy.it +lt.it +lu.it +lucania.it +lucca.it +macerata.it +mantova.it +mar.it +marche.it +massa-carrara.it +massacarrara.it +matera.it +mb.it +mc.it +me.it +medio-campidano.it +mediocampidano.it +messina.it +mi.it +milan.it +milano.it +mn.it +mo.it +modena.it +mol.it +molise.it +monza-brianza.it +monza-e-della-brianza.it +monza.it +monzabrianza.it +monzaebrianza.it +monzaedellabrianza.it +ms.it +mt.it +na.it +naples.it +napoli.it +no.it +novara.it +nu.it +nuoro.it +og.it +ogliastra.it +olbia-tempio.it +olbiatempio.it +or.it +oristano.it +ot.it +pa.it +padova.it +padua.it +palermo.it +parma.it +pavia.it +pc.it +pd.it +pe.it +perugia.it +pesaro-urbino.it +pesarourbino.it +pescara.it +pg.it +pi.it +piacenza.it +piedmont.it +piemonte.it +pisa.it +pistoia.it +pmn.it +pn.it +po.it +pordenone.it +potenza.it +pr.it +prato.it +pt.it +pu.it +pug.it +puglia.it +pv.it +pz.it +ra.it +ragusa.it +ravenna.it +rc.it +re.it +reggio-calabria.it +reggio-emilia.it +reggiocalabria.it +reggioemilia.it +rg.it +ri.it +rieti.it +rimini.it +rm.it +rn.it +ro.it +roma.it +rome.it +rovigo.it +sa.it +salerno.it +sar.it +sardegna.it +sardinia.it +sassari.it +savona.it +si.it +sic.it +sicilia.it +sicily.it +siena.it +siracusa.it +so.it +sondrio.it +sp.it +sr.it +ss.it +suedtirol.it +sv.it +ta.it +taa.it +taranto.it +te.it +tempio-olbia.it +tempioolbia.it +teramo.it +terni.it +tn.it +to.it +torino.it +tos.it +toscana.it +tp.it +tr.it +trani-andria-barletta.it +trani-barletta-andria.it +traniandriabarletta.it +tranibarlettaandria.it +trapani.it +trentino-a-adige.it +trentino-aadige.it +trentino-alto-adige.it +trentino-altoadige.it +trentino-s-tirol.it +trentino-stirol.it +trentino-sud-tirol.it +trentino-sudtirol.it +trentino-sued-tirol.it +trentino-suedtirol.it +trentino.it +trentinoa-adige.it +trentinoaadige.it +trentinoalto-adige.it +trentinoaltoadige.it +trentinos-tirol.it +trentinosud-tirol.it +trentinosudtirol.it +trentinosued-tirol.it +trentinosuedtirol.it +trento.it +treviso.it +trieste.it +ts.it +turin.it +tuscany.it +tv.it +ud.it +udine.it +umb.it +umbria.it +urbino-pesaro.it +urbinopesaro.it +va.it +val-d-aosta.it +val-daosta.it +vald-aosta.it +valdaosta.it +valle-d-aosta.it +valle-daosta.it +valled-aosta.it +valledaosta.it +vao.it +varese.it +vb.it +vc.it +vda.it +ve.it +ven.it +veneto.it +venezia.it +venice.it +verbania.it +vercelli.it +verona.it +vi.it +vibo-valentia.it +vibovalentia.it +vicenza.it +viterbo.it +vr.it +vs.it +vt.it +vv.it +co.je +net.je +org.je +com.jm +net.jm +org.jm +com.jo +name.jo +net.jo +org.jo +sch.jo +akita.jp +co.jp +gr.jp +kyoto.jp +ne.jp +or.jp +osaka.jp +saga.jp +tokyo.jp +ac.ke +co.ke +go.ke +info.ke +me.ke +mobi.ke +ne.ke +or.ke +sc.ke +com.kg +net.kg +org.kg +com.kh +edu.kh +net.kh +org.kh +biz.ki +com.ki +edu.ki +gov.ki +info.ki +mobi.ki +net.ki +org.ki +phone.ki +tel.ki +com.km +nom.km +org.km +tm.km +com.kn +co.kr +go.kr +ms.kr +ne.kr +or.kr +pe.kr +re.kr +seoul.kr +com.kw +edu.kw +net.kw +org.kw +com.ky +net.ky +org.ky +com.kz +org.kz +com.lb +edu.lb +net.lb +org.lb +co.lc +com.lc +l.lc +net.lc +org.lc +p.lc +com.lk +edu.lk +grp.lk +hotel.lk +ltd.lk +org.lk +soc.lk +web.lk +com.lr +org.lr +co.ls +net.ls +org.ls +asn.lv +com.lv +conf.lv +edu.lv +id.lv +mil.lv +net.lv +org.lv +com.ly +id.ly +med.ly +net.ly +org.ly +plc.ly +sch.ly +ac.ma +co.ma +net.ma +org.ma +press.ma +asso.mc +tm.mc +co.mg +com.mg +mil.mg +net.mg +nom.mg +org.mg +prd.mg +tm.mg +com.mk +edu.mk +inf.mk +name.mk +net.mk +org.mk +biz.mm +com.mm +net.mm +org.mm +per.mm +com.mo +net.mo +org.mo +edu.mr +org.mr +perso.mr +co.ms +com.ms +org.ms +com.mt +net.mt +org.mt +ac.mu +co.mu +com.mu +net.mu +nom.mu +or.mu +org.mu +com.mv +ac.mw +co.mw +com.mw +coop.mw +edu.mw +int.mw +net.mw +org.mw +com.mx +net.mx +org.mx +com.my +mil.my +name.my +net.my +org.my +co.mz +edu.mz +net.mz +org.mz +alt.na +co.na +com.na +edu.na +net.na +org.na +asso.nc +nom.nc +com.ne +info.ne +int.ne +org.ne +perso.ne +auz.net +gb.net +hu.net +in.net +jp.net +ru.net +se.net +uk.net +arts.nf +com.nf +firm.nf +info.nf +net.nf +org.nf +other.nf +per.nf +rec.nf +store.nf +web.nf +com.ng +edu.ng +gov.ng +i.ng +lg.gov.ng +mobi.ng +name.ng +net.ng +org.ng +sch.ng +ac.ni +biz.ni +co.ni +com.ni +edu.ni +gob.ni +in.ni +info.ni +int.ni +mil.ni +net.ni +nom.ni +org.ni +web.ni +co.nl +com.nl +net.nl +co.no +fhs.no +folkebibl.no +fylkesbibl.no +gs.no +idrett.no +museum.no +priv.no +uenorge.no +vgs.no +aero.np +asia.np +biz.np +com.np +coop.np +info.np +mil.np +mobi.np +museum.np +name.np +net.np +org.np +pro.np +travel.np +biz.nr +com.nr +info.nr +net.nr +org.nr +co.nu +ac.nz +co.net.nz +co.nz +geek.nz +gen.nz +iwi.nz +kiwi.nz +maori.nz +net.nz +org.nz +school.nz +biz.om +co.om +com.om +med.om +mil.om +museum.om +net.om +org.om +pro.om +sch.om +ae.org +hk.org +us.org +abo.pa +com.pa +edu.pa +gob.pa +ing.pa +med.pa +net.pa +nom.pa +org.pa +sld.pa +com.pe +gob.pe +mil.pe +net.pe +nom.pe +org.pe +asso.pf +com.pf +org.pf +com.pg +net.pg +org.pg +com.ph +net.ph +org.ph +biz.pk +com.pk +net.pk +org.pk +web.pk +agro.pl +aid.pl +atm.pl +augustow.pl +auto.pl +babia-gora.pl +bedzin.pl +beskidy.pl +bialowieza.pl +bialystok.pl +bielawa.pl +bieszczady.pl +biz.pl +boleslawiec.pl +bydgoszcz.pl +bytom.pl +cieszyn.pl +com.pl +czeladz.pl +czest.pl +dlugoleka.pl +edu.pl +elblag.pl +elk.pl +glogow.pl +gmina.pl +gniezno.pl +gorlice.pl +grajewo.pl +gsm.pl +ilawa.pl +info.pl +jaworzno.pl +jelenia-gora.pl +jgora.pl +kalisz.pl +karpacz.pl +kartuzy.pl +kaszuby.pl +katowice.pl +kazimierz-dolny.pl +kepno.pl +ketrzyn.pl +klodzko.pl +kobierzyce.pl +kolobrzeg.pl +konin.pl +konskowola.pl +kutno.pl +lapy.pl +lebork.pl +legnica.pl +lezajsk.pl +limanowa.pl +lomza.pl +lowicz.pl +lubin.pl +lukow.pl +mail.pl +malbork.pl +malopolska.pl +mazowsze.pl +mazury.pl +media.pl +miasta.pl +mielec.pl +mielno.pl +mil.pl +mragowo.pl +naklo.pl +net.pl +nieruchomosci.pl +nom.pl +nowaruda.pl +nysa.pl +olawa.pl +olecko.pl +olkusz.pl +olsztyn.pl +opoczno.pl +opole.pl +org.pl +ostroda.pl +ostroleka.pl +ostrowiec.pl +ostrowwlkp.pl +pc.pl +pila.pl +pisz.pl +podhale.pl +podlasie.pl +polkowice.pl +pomorskie.pl +pomorze.pl +powiat.pl +priv.pl +prochowice.pl +pruszkow.pl +przeworsk.pl +pulawy.pl +radom.pl +rawa-maz.pl +realestate.pl +rel.pl +rybnik.pl +rzeszow.pl +sanok.pl +sejny.pl +sex.pl +shop.pl +sklep.pl +skoczow.pl +slask.pl +slupsk.pl +sos.pl +sosnowiec.pl +stalowa-wola.pl +starachowice.pl +stargard.pl +suwalki.pl +swidnica.pl +swiebodzin.pl +swinoujscie.pl +szczecin.pl +szczytno.pl +szkola.pl +targi.pl +tarnobrzeg.pl +tgory.pl +tm.pl +tourism.pl +travel.pl +turek.pl +turystyka.pl +tychy.pl +ustka.pl +walbrzych.pl +warmia.pl +warszawa.pl +waw.pl +wegrow.pl +wielun.pl +wlocl.pl +wloclawek.pl +wodzislaw.pl +wolomin.pl +wroclaw.pl +zachpomor.pl +zagan.pl +zarow.pl +zgora.pl +zgorzelec.pl +co.pn +net.pn +org.pn +at.pr +biz.pr +ch.pr +com.pr +de.pr +eu.pr +fr.pr +info.pr +isla.pr +it.pr +name.pr +net.pr +nl.pr +org.pr +pro.pr +uk.pr +aaa.pro +aca.pro +acct.pro +arc.pro +avocat.pro +bar.pro +bus.pro +chi.pro +chiro.pro +cpa.pro +den.pro +dent.pro +eng.pro +jur.pro +law.pro +med.pro +min.pro +nur.pro +nurse.pro +pharma.pro +prof.pro +prx.pro +recht.pro +rel.pro +teach.pro +vet.pro +com.ps +net.ps +org.ps +co.pt +com.pt +org.pt +com.py +coop.py +edu.py +mil.py +net.py +org.py +com.qa +edu.qa +mil.qa +name.qa +net.qa +org.qa +sch.qa +com.re +arts.ro +co.ro +com.ro +firm.ro +info.ro +ne.ro +nom.ro +nt.ro +or.ro +org.ro +rec.ro +sa.ro +srl.ro +store.ro +tm.ro +www.ro +co.rs +edu.rs +in.rs +org.rs +adygeya.ru +bashkiria.ru +bir.ru +cbg.ru +com.ru +dagestan.ru +grozny.ru +kalmykia.ru +kustanai.ru +marine.ru +mordovia.ru +msk.ru +mytis.ru +nalchik.ru +net.ru +nov.ru +org.ru +pp.ru +pyatigorsk.ru +spb.ru +vladikavkaz.ru +vladimir.ru +ac.rw +co.rw +net.rw +org.rw +com.sa +edu.sa +med.sa +net.sa +org.sa +pub.sa +sch.sa +com.sb +net.sb +org.sb +com.sc +net.sc +org.sc +com.sd +info.sd +net.sd +com.se +com.sg +edu.sg +net.sg +org.sg +per.sg +ae.si +at.si +cn.si +co.si +de.si +uk.si +us.si +com.sl +edu.sl +net.sl +org.sl +art.sn +com.sn +edu.sn +org.sn +perso.sn +univ.sn +com.so +net.so +org.so +biz.ss +com.ss +me.ss +net.ss +abkhazia.su +adygeya.su +aktyubinsk.su +arkhangelsk.su +armenia.su +ashgabad.su +azerbaijan.su +balashov.su +bashkiria.su +bryansk.su +bukhara.su +chimkent.su +dagestan.su +east-kazakhstan.su +exnet.su +georgia.su +grozny.su +ivanovo.su +jambyl.su +kalmykia.su +kaluga.su +karacol.su +karaganda.su +karelia.su +khakassia.su +krasnodar.su +kurgan.su +kustanai.su +lenug.su +mangyshlak.su +mordovia.su +msk.su +murmansk.su +nalchik.su +navoi.su +north-kazakhstan.su +nov.su +obninsk.su +penza.su +pokrovsk.su +sochi.su +spb.su +tashkent.su +termez.su +togliatti.su +troitsk.su +tselinograd.su +tula.su +tuva.su +vladikavkaz.su +vladimir.su +vologda.su +com.sv +edu.sv +gob.sv +org.sv +com.sy +co.sz +org.sz +com.tc +net.tc +org.tc +pro.tc +com.td +net.td +org.td +tourism.td +ac.th +co.th +in.th +or.th +ac.tj +aero.tj +biz.tj +co.tj +com.tj +coop.tj +dyn.tj +go.tj +info.tj +int.tj +mil.tj +museum.tj +my.tj +name.tj +net.tj +org.tj +per.tj +pro.tj +web.tj +com.tl +net.tl +org.tl +agrinet.tn +com.tn +defense.tn +edunet.tn +ens.tn +fin.tn +ind.tn +info.tn +intl.tn +nat.tn +net.tn +org.tn +perso.tn +rnrt.tn +rns.tn +rnu.tn +tourism.tn +av.tr +bbs.tr +biz.tr +com.tr +dr.tr +gen.tr +info.tr +name.tr +net.tr +org.tr +tel.tr +tv.tr +web.tr +biz.tt +co.tt +com.tt +info.tt +jobs.tt +mobi.tt +name.tt +net.tt +org.tt +pro.tt +club.tw +com.tw +ebiz.tw +game.tw +idv.tw +net.tw +org.tw +ac.tz +co.tz +hotel.tz +info.tz +me.tz +mil.tz +mobi.tz +ne.tz +or.tz +sc.tz +tv.tz +biz.ua +cherkassy.ua +cherkasy.ua +chernigov.ua +chernivtsi.ua +chernovtsy.ua +ck.ua +cn.ua +co.ua +com.ua +crimea.ua +cv.ua +dn.ua +dnepropetrovsk.ua +dnipropetrovsk.ua +donetsk.ua +dp.ua +edu.ua +gov.ua +if.ua +in.ua +ivano-frankivsk.ua +kh.ua +kharkiv.ua +kharkov.ua +kherson.ua +khmelnitskiy.ua +kiev.ua +kirovograd.ua +km.ua +kr.ua +ks.ua +kyiv.ua +lg.ua +lt.ua +lugansk.ua +lutsk.ua +lviv.ua +mk.ua +net.ua +nikolaev.ua +od.ua +odesa.ua +odessa.ua +org.ua +pl.ua +poltava.ua +pp.ua +rivne.ua +rovno.ua +rv.ua +sebastopol.ua +sm.ua +sumy.ua +te.ua +ternopil.ua +uz.ua +uzhgorod.ua +vinnica.ua +vn.ua +volyn.ua +yalta.ua +zaporizhzhe.ua +zhitomir.ua +zp.ua +zt.ua +ac.ug +co.ug +com.ug +go.ug +ne.ug +or.ug +org.ug +sc.ug +ac.uk +barking-dagenham.sch.uk +barnet.sch.uk +barnsley.sch.uk +bathnes.sch.uk +beds.sch.uk +bexley.sch.uk +bham.sch.uk +blackburn.sch.uk +blackpool.sch.uk +bolton.sch.uk +bournemouth.sch.uk +bracknell-forest.sch.uk +bradford.sch.uk +brent.sch.uk +co.uk +doncaster.sch.uk +gov.uk +ltd.uk +me.uk +net.uk +org.uk +plc.uk +sch.uk +com.uy +edu.uy +net.uy +org.uy +biz.uz +co.uz +com.uz +net.uz +org.uz +com.vc +net.vc +org.vc +co.ve +com.ve +info.ve +net.ve +org.ve +web.ve +co.vi +com.vi +net.vi +org.vi +ac.vn +biz.vn +com.vn +edu.vn +health.vn +info.vn +int.vn +name.vn +net.vn +org.vn +pro.vn +com.vu +net.vu +org.vu +com.ws +net.ws +org.ws +com.ye +net.ye +org.ye +co.za +net.za +org.za +web.za +co.zm +com.zm +org.zm +co.zw +ком.рф +нет.рф +орг.рф +ак.срб +пр.срб +упр.срб +كمپنی.بھارت +कंपनी.भारत +কোম্পানি.ভারত +நிறுவனம்.இந்தியா +個人.香港 +公司.香港 +政府.香港 +教育.香港 +組織.香港 +網絡.香港 +gov.cn +com.ac +com.ad +com.ae +com.an +com.ao +com.aq +com.as +com.at +com.aw +com.ba +com.be +com.bf +com.bg +com.bv +com.bw +com.ca +com.cc +com.cf +com.cg +com.ch +com.ck +com.cl +com.cq +com.cr +com.cx +com.cz +com.dj +com.dk +com.eh +com.eu +com.ev +com.fi +com.fk +com.fm +com.fo +com.ga +com.gb +com.gd +com.gf +com.gm +com.gw +com.hm +com.hu +com.id +com.ie +com.il +com.in +com.io +com.ir +com.is +com.it +com.jp +com.ke +com.kp +com.kr +com.la +com.li +com.ls +com.lt +com.lu +com.ma +com.mc +com.md +com.me +com.mh +com.ml +com.mn +com.mp +com.mq +com.mr +com.mz +com.nc +com.no +com.nt +com.nu +com.nz +com.pm +com.pn +com.pw +com.rs +com.rw +com.sh +com.si +com.sj +com.sk +com.sm +com.sr +com.st +com.su +com.sz +com.tf +com.tg +com.th +com.tk +com.tm +com.to +com.tp +com.tv +com.tz +com.uk +com.us +com.va +com.vg +com.wf +com.za +com.zw +mil.cn +edu.kg +edu.cn \ No newline at end of file diff --git a/app/lib/CertHelper.php b/app/lib/CertHelper.php new file mode 100644 index 0000000..5fbf340 --- /dev/null +++ b/app/lib/CertHelper.php @@ -0,0 +1,349 @@ + [ + 'name' => 'Let\'s Encrypt', + 'class' => 1, + 'icon' => 'letsencrypt.ico', + 'wildcard' => true, + 'max_domains' => 100, + 'cname' => true, + 'note' => null, + 'inputs' => [ + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => '用于注册Let\'s Encrypt账号', + 'required' => true, + ], + 'mode' => [ + 'name' => '环境选择', + 'type' => 'radio', + 'options' => [ + 'live' => '正式环境', + 'staging' => '测试环境', + ], + 'value' => 'live' + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ] + ], + 'zerossl' => [ + 'name' => 'ZeroSSL', + 'class' => 1, + 'icon' => 'zerossl.ico', + 'wildcard' => true, + 'max_domains' => 100, + 'cname' => true, + 'note' => 'ZeroSSL密钥生成地址', + 'inputs' => [ + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => 'EAB申请邮箱', + 'required' => true, + ], + 'kid' => [ + 'name' => 'EAB KID', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'key' => [ + 'name' => 'EAB HMAC Key', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ] + ], + 'google' => [ + 'name' => 'Google SSL', + 'class' => 1, + 'icon' => 'google.ico', + 'wildcard' => true, + 'max_domains' => 100, + 'cname' => true, + 'note' => '查看Google SSL账户配置说明', + 'inputs' => [ + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => 'EAB申请邮箱', + 'required' => true, + ], + 'kid' => [ + 'name' => 'keyId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'key' => [ + 'name' => 'b64MacKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'mode' => [ + 'name' => '环境选择', + 'type' => 'radio', + 'options' => [ + 'live' => '正式环境', + 'staging' => '测试环境', + ], + 'value' => 'live' + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ] + ], + 'tencent' => [ + 'name' => '腾讯云免费SSL', + 'class' => 2, + 'icon' => 'tencent.ico', + 'wildcard' => false, + 'max_domains' => 1, + 'cname' => false, + 'note' => '一个账号有50张免费证书额度,证书到期或吊销可释放额度。腾讯云免费SSL简介与额度说明', + 'inputs' => [ + 'SecretId' => [ + 'name' => 'SecretId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'SecretKey' => [ + 'name' => 'SecretKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => '申请证书时填写的邮箱', + 'required' => true, + ], + ] + ], + 'aliyun' => [ + 'name' => '阿里云免费SSL', + 'class' => 2, + 'icon' => 'aliyun.ico', + 'wildcard' => false, + 'max_domains' => 1, + 'cname' => false, + 'note' => '每个自然年有20张免费证书额度,证书到期或吊销不释放额度。需要先进入阿里云控制台-数字证书管理服务,购买个人测试证书资源包。', + 'inputs' => [ + 'AccessKeyId' => [ + 'name' => 'AccessKeyId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'AccessKeySecret' => [ + 'name' => 'AccessKeySecret', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'username' => [ + 'name' => '姓名', + 'type' => 'input', + 'placeholder' => '申请联系人的姓名', + 'required' => true, + ], + 'phone' => [ + 'name' => '手机号码', + 'type' => 'input', + 'placeholder' => '申请联系人的手机号码', + 'required' => true, + ], + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => '申请联系人的邮箱地址', + 'required' => true, + ], + ] + ], + 'ucloud' => [ + 'name' => 'UCloud免费SSL', + 'class' => 2, + 'icon' => 'ucloud.ico', + 'wildcard' => false, + 'max_domains' => 1, + 'cname' => false, + 'note' => '一个账号有40张免费证书额度,证书到期或吊销可释放额度。', + 'inputs' => [ + 'PublicKey' => [ + 'name' => '公钥', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'PrivateKey' => [ + 'name' => '私钥', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'username' => [ + 'name' => '姓名', + 'type' => 'input', + 'placeholder' => '申请联系人的姓名', + 'required' => true, + ], + 'phone' => [ + 'name' => '手机号码', + 'type' => 'input', + 'placeholder' => '申请联系人的手机号码', + 'required' => true, + ], + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => '申请联系人的邮箱地址', + 'required' => true, + ], + ] + ], + 'customacme' => [ + 'name' => '自定义ACME', + 'class' => 1, + 'icon' => 'ssl.ico', + 'wildcard' => true, + 'max_domains' => 100, + 'cname' => true, + 'note' => null, + 'inputs' => [ + 'directory' => [ + 'name' => 'ACME地址', + 'type' => 'input', + 'placeholder' => 'ACME Directory 地址', + 'required' => true, + ], + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => '证书申请邮箱', + 'required' => true, + ], + 'kid' => [ + 'name' => 'EAB KID', + 'type' => 'input', + 'placeholder' => '留空则不使用EAB认证', + ], + 'key' => [ + 'name' => 'EAB HMAC Key', + 'type' => 'input', + 'placeholder' => '留空则不使用EAB认证', + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ] + ], + ]; + + public static $class_config = [ + 1 => '基于ACME的SSL证书', + 2 => '云服务商的SSL证书', + ]; + + public static function getList() + { + return self::$cert_config; + } + + private static function getConfig($aid) + { + $account = Db::name('cert_account')->where('id', $aid)->find(); + if (!$account) return false; + return $account; + } + + public static function getInputs($type, $config = null) + { + $config = $config ? json_decode($config, true) : []; + $inputs = self::$cert_config[$type]['inputs']; + foreach ($inputs as &$input) { + if (isset($config[$input['name']])) { + $input['value'] = $config[$input['name']]; + } + } + return $inputs; + } + + /** + * @return CertInterface|bool + */ + public static function getModel($aid) + { + $account = self::getConfig($aid); + if (!$account) return false; + $type = $account['type']; + $class = "\\app\\lib\\cert\\{$type}"; + if (class_exists($class)) { + $config = json_decode($account['config'], true); + $ext = $account['ext'] ? json_decode($account['ext'], true) : null; + $model = new $class($config, $ext); + return $model; + } + return false; + } + + /** + * @return CertInterface|bool + */ + public static function getModel2($type, $config, $ext = null) + { + $class = "\\app\\lib\\cert\\{$type}"; + if (class_exists($class)) { + $model = new $class($config, $ext); + return $model; + } + return false; + } + + public static function getPfx($fullchain, $privatekey, $pwd = '123456'){ + openssl_pkcs12_export($fullchain, $pfx, $privatekey, $pwd); + return $pfx; + } +} diff --git a/app/lib/CertInterface.php b/app/lib/CertInterface.php new file mode 100644 index 0000000..00346c2 --- /dev/null +++ b/app/lib/CertInterface.php @@ -0,0 +1,24 @@ + [ + 'name' => '宝塔面板', + 'class' => 1, + 'icon' => 'bt.ico', + 'note' => null, + 'inputs' => [ + 'url' => [ + 'name' => '面板地址', + 'type' => 'input', + 'placeholder' => '宝塔面板地址', + 'note' => '填写规则如:http://192.168.1.100:8888 ,不要带其他后缀', + 'required' => true, + ], + 'key' => [ + 'name' => '接口密钥', + 'type' => 'input', + 'placeholder' => '宝塔面板设置->面板设置->API接口', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'type' => [ + 'name' => '部署类型', + 'type' => 'radio', + 'options' => [ + '0' => '宝塔面板站点的证书', + '1' => '宝塔面板本身的证书', + ], + 'value' => '0', + 'required' => true, + ], + 'sites' => [ + 'name' => '网站名称列表', + 'type' => 'textarea', + 'placeholder' => '填写要部署证书的网站名称,每行一个', + 'note' => 'PHP项目和反代项目填写创建时绑定的第一个域名,Java/Node/Go等其他项目填写项目名称', + 'show' => 'type==0', + 'required' => true, + ], + ], + ], + 'kangle' => [ + 'name' => 'Kangle', + 'class' => 1, + 'icon' => 'host.png', + 'note' => '以上登录信息为Easypanel用户面板的,非管理员面板。如选网站密码认证类型,则用户面板登录不能开启验证码。', + 'inputs' => [ + 'url' => [ + 'name' => '面板地址', + 'type' => 'input', + 'placeholder' => 'Easypanel面板地址', + 'note' => '填写规则如:http://192.168.1.100:3312 ,不要带其他后缀', + 'required' => true, + ], + 'auth' => [ + 'name' => '认证方式', + 'type' => 'radio', + 'options' => [ + '0' => '网站密码', + '1' => '面板安全码', + ], + 'value' => '0', + 'required' => true, + ], + 'username' => [ + 'name' => '网站用户名', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'password' => [ + 'name' => '网站密码', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'auth==0', + 'required' => true, + ], + 'skey' => [ + 'name' => '面板安全码', + 'type' => 'input', + 'placeholder' => '管理员面板->服务器设置->面板通信安全码', + 'show' => 'auth==1', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'type' => [ + 'name' => '部署类型', + 'type' => 'radio', + 'options' => [ + '0' => '网站SSL证书', + '1' => '单域名SSL证书(仅CDN支持)', + ], + 'value' => '0', + 'required' => true, + ], + 'domains' => [ + 'name' => 'CDN域名列表', + 'type' => 'textarea', + 'placeholder' => '填写要部署证书的域名,每行一个', + 'show' => 'type==1', + 'required' => true, + ], + ], + ], + 'safeline' => [ + 'name' => '雷池WAF', + 'class' => 1, + 'icon' => 'safeline.png', + 'note' => null, + 'tasknote' => '系统会根据关联SSL证书的域名,自动更新对应证书', + 'inputs' => [ + 'url' => [ + 'name' => '控制台地址', + 'type' => 'input', + 'placeholder' => '雷池WAF控制台地址', + 'note' => '填写规则如:https://192.168.1.100:9443 ,不要带其他后缀', + 'required' => true, + ], + 'token' => [ + 'name' => 'API Token', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [], + ], + 'cdnfly' => [ + 'name' => 'Cdnfly', + 'class' => 1, + 'icon' => 'waf.png', + 'note' => '登录Cdnfly控制台->账户中心->API密钥,点击开启后获取', + 'inputs' => [ + 'url' => [ + 'name' => '控制台地址', + 'type' => 'input', + 'placeholder' => 'Cdnfly控制台地址', + 'note' => '填写示例:http://demo.cdnfly.cn', + 'required' => true, + ], + 'api_key' => [ + 'name' => 'api_key', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'api_secret' => [ + 'name' => 'api_secret', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'id' => [ + 'name' => '证书ID', + 'type' => 'input', + 'placeholder' => '', + 'note' => '在网站管理->证书管理查看证书的ID,注意域名是否与证书匹配', + 'required' => true, + ], + ], + ], + 'lecdn' => [ + 'name' => 'LeCDN', + 'class' => 1, + 'icon' => 'waf.png', + 'note' => null, + 'inputs' => [ + 'url' => [ + 'name' => '控制台地址', + 'type' => 'input', + 'placeholder' => 'LeCDN控制台地址', + 'note' => '填写示例:http://demo.xxxx.cn', + 'required' => true, + ], + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'password' => [ + 'name' => '密码', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'id' => [ + 'name' => '证书ID', + 'type' => 'input', + 'placeholder' => '', + 'note' => '在站点->证书管理查看证书的ID,注意域名是否与证书匹配', + 'required' => true, + ], + ], + ], + 'goedge' => [ + 'name' => 'GoEdge', + 'class' => 1, + 'icon' => 'waf.png', + 'note' => '需要先开启HTTP API端口', + 'tasknote' => '系统会根据关联SSL证书的域名,自动更新对应证书', + 'inputs' => [ + 'systype' => [ + 'name' => '系统类型', + 'type' => 'radio', + 'options' => [ + '0' => 'GoEdge', + '1' => 'FlexCDN', + ], + 'value' => '0', + 'required' => true, + ], + 'url' => [ + 'name' => 'HTTP API地址', + 'type' => 'input', + 'placeholder' => 'HTTP API地址', + 'note' => 'http://你的IP:端口', + 'required' => true, + ], + 'accessKeyId' => [ + 'name' => 'AccessKey ID', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'accessKey' => [ + 'name' => 'AccessKey密钥', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'usertype' => [ + 'name' => '用户类型', + 'type' => 'radio', + 'options' => [ + 'user' => '平台用户', + 'admin' => '系统用户', + ], + 'value' => 'user', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [], + ], + 'opanel' => [ + 'name' => '1Panel', + 'class' => 1, + 'icon' => 'opanel.png', + 'note' => null, + 'tasknote' => '系统会根据关联SSL证书的域名,自动更新对应证书', + 'inputs' => [ + 'url' => [ + 'name' => '面板地址', + 'type' => 'input', + 'placeholder' => '1Panel面板地址', + 'note' => '填写规则如:http://192.168.1.100:8888 ,不要带其他后缀', + 'required' => true, + ], + 'key' => [ + 'name' => '接口密钥', + 'type' => 'input', + 'placeholder' => '1Panel面板设置->API接口', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [], + ], + 'aliyun' => [ + 'name' => '阿里云', + 'class' => 2, + 'icon' => 'aliyun.ico', + 'note' => '支持部署到阿里云CDN、ESA、SLB、OSS、WAF等服务', + 'tasknote' => '', + 'inputs' => [ + 'AccessKeyId' => [ + 'name' => 'AccessKeyId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'AccessKeySecret' => [ + 'name' => 'AccessKeySecret', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + 'taskinputs' => [ + 'product' => [ + 'name' => '要部署的产品', + 'type' => 'select', + 'options' => [ + ['value'=>'cdn', 'label'=>'内容分发CDN'], + ['value'=>'dcdn', 'label'=>'全站加速DCDN'], + ['value'=>'esa', 'label'=>'边缘安全加速ESA'], + ['value'=>'oss', 'label'=>'对象存储OSS'], + ['value'=>'waf', 'label'=>'Web应用防火墙3.0'], + ['value'=>'waf2', 'label'=>'Web应用防火墙2.0'], + ['value'=>'clb', 'label'=>'传统型负载均衡CLB'], + ['value'=>'alb', 'label'=>'应用型负载均衡ALB'], + ['value'=>'nlb', 'label'=>'网络型负载均衡NLB'], + ['value'=>'api', 'label'=>'API网关'], + ['value'=>'ddoscoo', 'label'=>'DDoS高防'], + ['value'=>'live', 'label'=>'视频直播'], + ['value'=>'vod', 'label'=>'视频点播'], + ['value'=>'fc', 'label'=>'函数计算3.0'], + ['value'=>'fc2', 'label'=>'函数计算2.0'], + ], + 'value' => 'cdn', + 'required' => true, + ], + 'esa_sitename' => [ + 'name' => 'ESA站点域名', + 'type' => 'input', + 'placeholder' => 'ESA添加的站点主域名', + 'show' => 'product==\'esa\'', + 'required' => true, + ], + 'oss_endpoint' => [ + 'name' => 'Endpoint地址', + 'type' => 'input', + 'placeholder' => '填写示例:oss-cn-hangzhou.aliyuncs.com', + 'show' => 'product==\'oss\'', + 'required' => true, + ], + 'oss_bucket' => [ + 'name' => 'Bucket名称', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'oss\'', + 'required' => true, + ], + 'region' => [ + 'name' => '所属地域', + 'type' => 'select', + 'options' => [ + ['value'=>'cn-hangzhou', 'label'=>'中国内地'], + ['value'=>'ap-southeast-1', 'label'=>'非中国内地'], + ], + 'value' => 'cn-hangzhou', + 'show' => 'product==\'waf\'||product==\'waf2\'||product==\'ddoscoo\'', + 'required' => true, + ], + 'regionid' => [ + 'name' => '所属地域ID', + 'type' => 'input', + 'placeholder' => '填写示例:cn-hangzhou', + 'show' => 'product==\'api\'||product==\'clb\'||product==\'alb\'||product==\'nlb\'', + 'value' => 'cn-hangzhou', + 'required' => true, + ], + 'api_groupid' => [ + 'name' => 'API分组ID', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'api\'', + 'required' => true, + ], + 'fc_cname' => [ + 'name' => '域名CNAME地址', + 'type' => 'input', + 'placeholder' => '填写示例:<账号ID>.cn-shanghai.fc.aliyuncs.com', + 'show' => 'product==\'fc\'||product==\'fc2\'', + 'required' => true, + ], + 'clb_id' => [ + 'name' => '负载均衡实例ID', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'clb\'', + 'required' => true, + ], + 'clb_port' => [ + 'name' => 'HTTPS监听端口', + 'type' => 'input', + 'placeholder' => '', + 'value' => '443', + 'show' => 'product==\'clb\'', + 'required' => true, + ], + 'alb_listener_id' => [ + 'name' => '监听ID', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'alb\'', + 'note' => '进入ALB实例详情->监听列表,复制监听ID(只支持HTTPS或QUIC监听协议)', + 'required' => true, + ], + 'nlb_listener_id' => [ + 'name' => '监听ID', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'nlb\'', + 'note' => '进入NLB实例详情->监听列表,复制监听ID(只支持TCPSSL监听协议)', + 'required' => true, + ], + 'domain' => [ + 'name' => '绑定的域名', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product!=\'esa\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'', + 'required' => true, + ], + ], + ], + 'tencent' => [ + 'name' => '腾讯云', + 'class' => 2, + 'icon' => 'tencent.ico', + 'note' => '支持部署到腾讯云CDN、EO、CLB、COS、TKE、SCF等服务', + 'tasknote' => '', + 'inputs' => [ + 'SecretId' => [ + 'name' => 'SecretId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'SecretKey' => [ + 'name' => 'SecretKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + 'taskinputs' => [ + 'product' => [ + 'name' => '要部署的产品', + 'type' => 'select', + 'options' => [ + ['value'=>'cdn', 'label'=>'内容分发网络CDN'], + ['value'=>'teo', 'label'=>'边缘安全加速EO'], + ['value'=>'waf', 'label'=>'Web应用防火墙WAF'], + ['value'=>'cos', 'label'=>'对象存储COS'], + ['value'=>'clb', 'label'=>'负载均衡CLB'], + ['value'=>'tke', 'label'=>'容器服务TKE'], + ['value'=>'scf', 'label'=>'云函数SCF'], + ['value'=>'ddos', 'label'=>'DDoS防护'], + ['value'=>'live', 'label'=>'云直播LIVE'], + ['value'=>'vod', 'label'=>'云点播VOD'], + ['value'=>'tse', 'label'=>'云原生API网关TSE'], + ['value'=>'tcb', 'label'=>'云开发TCB'], + ['value'=>'lighthouse', 'label'=>'轻量应用服务器'], + ], + 'value' => 'cdn', + 'required' => true, + ], + 'regionid' => [ + 'name' => '所属地域ID', + 'type' => 'input', + 'placeholder' => '填写示例:ap-guangzhou', + 'show' => 'product==\'clb\'||product==\'cos\'||product==\'tse\'||product==\'tke\'||product==\'lighthouse\'||product==\'scf\'', + 'value' => '', + 'required' => true, + ], + 'region' => [ + 'name' => '所属地域', + 'type' => 'select', + 'options' => [ + ['value'=>'ap-guangzhou', 'label'=>'中国大陆'], + ['value'=>'ap-seoul', 'label'=>'非中国大陆'], + ], + 'value' => 'ap-guangzhou', + 'show' => 'product==\'waf\'', + 'required' => true, + ], + 'clb_id' => [ + 'name' => '负载均衡ID', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'clb\'', + 'required' => true, + ], + 'clb_listener_id' => [ + 'name' => '监听器ID', + 'type' => 'input', + 'placeholder' => '可留空,会自动根据域名或负载均衡ID查找', + 'show' => 'product==\'clb\'', + ], + 'clb_domain' => [ + 'name' => '绑定的域名', + 'type' => 'input', + 'placeholder' => '若监听器开启SNI,则域名必填;若关闭SNI,则域名留空', + 'show' => 'product==\'clb\'', + ], + 'tke_cluster_id' => [ + 'name' => '集群ID', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'tke\'', + 'required' => true, + ], + 'tke_namespace' => [ + 'name' => '命名空间', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'tke\'', + 'required' => true, + ], + 'tke_secret' => [ + 'name' => '证书的secret名称', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'tke\'', + 'required' => true, + ], + 'cos_bucket' => [ + 'name' => '存储桶名称', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'cos\'', + 'required' => true, + ], + 'lighthouse_id' => [ + 'name' => '实例ID', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'lighthouse\'', + 'required' => true, + ], + 'domain' => [ + 'name' => '绑定的域名', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product!=\'clb\'&&product!=\'tke\'', + 'required' => true, + ], + ], + ], + 'huawei' => [ + 'name' => '华为云', + 'class' => 2, + 'icon' => 'huawei.ico', + 'note' => '支持部署到华为云CDN、ELB、WAF等服务', + 'inputs' => [ + 'AccessKeyId' => [ + 'name' => 'AccessKeyId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'SecretAccessKey' => [ + 'name' => 'SecretAccessKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + 'taskinputs' => [ + 'product' => [ + 'name' => '要部署的产品', + 'type' => 'select', + 'options' => [ + ['value'=>'cdn', 'label'=>'内容分发网络CDN'], + ['value'=>'elb', 'label'=>'弹性负载均衡ELB'], + ['value'=>'waf', 'label'=>'Web应用防火墙WAF'], + ], + 'value' => 'cdn', + 'required' => true, + ], + 'domain' => [ + 'name' => '绑定的域名', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'cdn\'', + 'required' => true, + ], + 'project_id' => [ + 'name' => '项目ID', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'elb\'||product==\'waf\'', + 'note' => '项目ID可在我的凭证->项目列表页面查看', + 'required' => true, + ], + 'region_id' => [ + 'name' => '区域ID', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'elb\'||product==\'waf\'', + 'note' => '区域ID可在此页面查找', + 'required' => true, + ], + 'cert_id' => [ + 'name' => '要更新的证书ID', + 'type' => 'input', + 'placeholder' => '在ELB控制台->证书管理->查看已上传的证书ID', + 'show' => 'product==\'elb\'||product==\'waf\'', + 'required' => true, + ], + ], + ], + 'ucloud' => [ + 'name' => 'UCloud', + 'class' => 2, + 'icon' => 'ucloud.ico', + 'note' => '支持部署到UCDN', + 'inputs' => [ + 'PublicKey' => [ + 'name' => '公钥', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'PrivateKey' => [ + 'name' => '私钥', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + 'taskinputs' => [ + 'domain_id' => [ + 'name' => '云分发资源ID', + 'type' => 'input', + 'placeholder' => '', + 'note' => '在云分发-域名管理-域名基本信息页面查看', + 'required' => true, + ], + ], + ], + 'qiniu' => [ + 'name' => '七牛云', + 'class' => 2, + 'icon' => 'qiniu.ico', + 'note' => '支持部署到七牛云CDN、OSS', + 'inputs' => [ + 'AccessKey' => [ + 'name' => 'AccessKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'SecretKey' => [ + 'name' => 'SecretKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + 'taskinputs' => [ + 'product' => [ + 'name' => '要部署的产品', + 'type' => 'select', + 'options' => [ + ['value'=>'cdn', 'label'=>'CDN'], + ['value'=>'oss', 'label'=>'OSS'], + ], + 'value' => 'cdn', + 'required' => true, + ], + 'domain' => [ + 'name' => '绑定的域名', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + ], + 'doge' => [ + 'name' => '多吉云', + 'class' => 2, + 'icon' => 'cloud.png', + 'note' => '支持部署到多吉云融合CDN', + 'inputs' => [ + 'AccessKey' => [ + 'name' => 'AccessKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'SecretKey' => [ + 'name' => 'SecretKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + 'taskinputs' => [ + 'domain' => [ + 'name' => 'CDN域名', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + ], + 'baidu' => [ + 'name' => '百度云', + 'class' => 2, + 'icon' => 'baidu.ico', + 'note' => '支持部署到百度云CDN', + 'inputs' => [ + 'AccessKeyId' => [ + 'name' => 'AccessKeyId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'SecretAccessKey' => [ + 'name' => 'SecretAccessKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + 'taskinputs' => [ + 'domain' => [ + 'name' => '绑定的域名', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + ], + 'huoshan' => [ + 'name' => '火山引擎', + 'class' => 2, + 'icon' => 'huoshan.ico', + 'note' => '支持部署到火山引擎CDN', + 'inputs' => [ + 'AccessKeyId' => [ + 'name' => 'AccessKeyId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'SecretAccessKey' => [ + 'name' => 'SecretAccessKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + 'taskinputs' => [ + 'domain' => [ + 'name' => '绑定的域名', + 'type' => 'input', + 'placeholder' => '多个域名可使用英文逗号分隔', + 'required' => true, + ], + ], + ], + 'baishan' => [ + 'name' => '白山云', + 'class' => 2, + 'icon' => 'waf.png', + 'note' => null, + 'inputs' => [ + 'account' => [ + 'name' => '账户名', + 'type' => 'input', + 'placeholder' => '仅用作标记', + 'required' => true, + ], + 'token' => [ + 'name' => 'token', + 'type' => 'input', + 'placeholder' => '', + 'note' => '自行联系提供商申请', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'id' => [ + 'name' => '证书ID', + 'type' => 'input', + 'placeholder' => '', + 'note' => '在证书管理页面查看,注意域名是否与证书匹配', + 'required' => true, + ], + ], + ], + 'allwaf' => [ + 'name' => 'AllWAF', + 'class' => 2, + 'icon' => 'waf.png', + 'note' => '在ALLWAF访问控制页面创建AccessKey', + 'tasknote' => '系统会根据关联SSL证书的域名,自动更新对应证书', + 'inputs' => [ + 'accessKeyId' => [ + 'name' => 'AccessKey ID', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'accessKey' => [ + 'name' => 'AccessKey密钥', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [], + ], + 'aws' => [ + 'name' => 'AWS', + 'class' => 2, + 'icon' => 'aws.ico', + 'note' => '支持部署到Amazon CloudFront', + 'inputs' => [ + 'AccessKeyId' => [ + 'name' => 'AccessKeyId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'SecretAccessKey' => [ + 'name' => 'SecretAccessKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + 'taskinputs' => [ + 'product' => [ + 'name' => '要部署的产品', + 'type' => 'select', + 'options' => [ + ['value'=>'cloudfront', 'label'=>'CloudFront'], + ], + 'value' => 'cloudfront', + 'required' => true, + ], + 'distribution_id' => [ + 'name' => '分配ID', + 'type' => 'input', + 'placeholder' => 'distributions id', + 'required' => true, + ], + ], + ], + 'gcore' => [ + 'name' => 'Gcore', + 'class' => 2, + 'icon' => 'gcore.ico', + 'note' => '在 个人资料->API令牌 页面创建API令牌', + 'inputs' => [ + 'account' => [ + 'name' => '账户名', + 'type' => 'input', + 'placeholder' => '仅用作标记', + 'required' => true, + ], + 'apikey' => [ + 'name' => 'API令牌', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'id' => [ + 'name' => '证书ID', + 'type' => 'input', + 'placeholder' => '', + 'note' => '在CDN->SSL证书页面查看证书的ID,注意域名是否与证书匹配', + 'required' => true, + ], + 'name' => [ + 'name' => '证书名称', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + ], + ], + 'cachefly' => [ + 'name' => 'Cachefly', + 'class' => 2, + 'icon' => 'cloud.png', + 'note' => '在 API Tokens 页面生成 API Token', + 'inputs' => [ + 'account' => [ + 'name' => '账户名', + 'type' => 'input', + 'placeholder' => '仅用作标记', + 'required' => true, + ], + 'apikey' => [ + 'name' => 'API Token', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + + ], + ], + 'ssh' => [ + 'name' => 'SSH服务器', + 'class' => 3, + 'icon' => 'server.png', + 'note' => '可通过SSH连接到Linux/Windows服务器并部署证书,php需要安装ssh2扩展', + 'tasknote' => '请确保路径存在且有写入权限,路径一定要以/开头(Windows路径请使用/代替\,且需要在最开头加/)', + 'inputs' => [ + 'host' => [ + 'name' => '主机地址', + 'type' => 'input', + 'placeholder' => '填写IP地址或域名', + 'required' => true, + ], + 'port' => [ + 'name' => '端口', + 'type' => 'input', + 'placeholder' => '', + 'value' => '22', + 'required' => true, + ], + 'auth' => [ + 'name' => '认证方式', + 'type' => 'radio', + 'options' => [ + '0' => '密码认证', + '1' => '密钥认证', + ], + 'value' => '0', + 'required' => true, + ], + 'username' => [ + 'name' => '用户名', + 'type' => 'input', + 'placeholder' => '登录用户名', + 'value' => 'root', + 'required' => true, + ], + 'password' => [ + 'name' => '密码', + 'type' => 'input', + 'placeholder' => '登录密码', + 'required' => true, + 'show' => 'auth==0', + ], + 'privatekey' => [ + 'name' => '私钥', + 'type' => 'textarea', + 'placeholder' => '填写私钥内容', + 'required' => true, + 'show' => 'auth==1', + ], + 'windows' => [ + 'name' => '是否Windows', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'note' => 'Windows系统需要先安装OpenSSH', + 'value' => '0', + 'required' => true, + ], + ], + 'taskinputs' => [ + 'format' => [ + 'name' => '证书类型', + 'type' => 'select', + 'options' => [ + ['value'=>'pem', 'label'=>'PEM格式(Nginx/Apache等)'], + ['value'=>'pfx', 'label'=>'PFX格式(IIS/Tomcat)'], + ], + 'value' => 'pem', + 'required' => true, + ], + 'pem_cert_file' => [ + 'name' => '证书保存路径', + 'type' => 'input', + 'placeholder' => '/path/to/cert.pem', + 'show' => 'format==\'pem\'', + 'required' => true, + ], + 'pem_key_file' => [ + 'name' => '私钥保存路径', + 'type' => 'input', + 'placeholder' => '/path/to/key.pem', + 'show' => 'format==\'pem\'', + 'required' => true, + ], + 'pfx_file' => [ + 'name' => 'PFX证书保存路径', + 'type' => 'input', + 'placeholder' => '/path/to/cert.pfx', + 'show' => 'format==\'pfx\'', + 'required' => true, + ], + 'pfx_pass' => [ + 'name' => 'PFX证书密码', + 'type' => 'input', + 'placeholder' => '留空为不设置密码', + 'show' => 'format==\'pfx\'', + ], + 'uptype' => [ + 'name' => '上传完操作', + 'type' => 'radio', + 'options' => [ + '0' => '执行指定命令', + '1' => '部署到IIS', + ], + 'value' => '0', + 'show' => 'format==\'pfx\'', + 'required' => true, + ], + 'cmd' => [ + 'name' => '上传完执行命令', + 'type' => 'textarea', + 'show' => 'format==\'pem\'||uptype==0', + 'placeholder' => '可留空,每行一条命令,如:service nginx reload', + ], + 'iis_domain' => [ + 'name' => '绑定的域名', + 'type' => 'input', + 'placeholder' => '在IIS站点绑定的https域名', + 'show' => 'format==\'pfx\'&&uptype==1', + ], + ], + ], + 'ftp' => [ + 'name' => 'FTP服务器', + 'class' => 3, + 'icon' => 'server.png', + 'note' => '可将证书上传到FTP服务器,php需要安装ftp扩展', + 'tasknote' => '请确保路径存在且有写入权限', + 'inputs' => [ + 'host' => [ + 'name' => 'FTP地址', + 'type' => 'input', + 'placeholder' => '填写IP地址或域名', + 'required' => true, + ], + 'port' => [ + 'name' => 'FTP端口', + 'type' => 'input', + 'placeholder' => '', + 'value' => '21', + 'required' => true, + ], + 'username' => [ + 'name' => '用户名', + 'type' => 'input', + 'placeholder' => 'FTP登录用户名', + 'required' => true, + ], + 'password' => [ + 'name' => '密码', + 'type' => 'input', + 'placeholder' => 'FTP登录密码', + 'required' => true, + ], + 'secure' => [ + 'name' => '是否使用SSL', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0', + 'required' => true, + ], + ], + 'taskinputs' => [ + 'format' => [ + 'name' => '证书类型', + 'type' => 'select', + 'options' => [ + ['value'=>'pem', 'label'=>'PEM格式(Nginx/Apache等)'], + ['value'=>'pfx', 'label'=>'PFX格式(IIS)'], + ], + 'value' => 'pem', + 'required' => true, + ], + 'pem_cert_file' => [ + 'name' => '证书保存路径', + 'type' => 'input', + 'placeholder' => '/path/to/cert.pem', + 'show' => 'format==\'pem\'', + 'required' => true, + ], + 'pem_key_file' => [ + 'name' => '私钥保存路径', + 'type' => 'input', + 'placeholder' => '/path/to/key.pem', + 'show' => 'format==\'pem\'', + 'required' => true, + ], + 'pfx_file' => [ + 'name' => 'PFX证书保存路径', + 'type' => 'input', + 'placeholder' => '/path/to/cert.pfx', + 'show' => 'format==\'pfx\'', + 'required' => true, + ], + 'pfx_pass' => [ + 'name' => 'PFX证书密码', + 'type' => 'input', + 'placeholder' => '留空为不设置密码', + 'show' => 'format==\'pfx\'', + ], + ], + ], + 'local' => [ + 'name' => '复制到本机', + 'class' => 3, + 'icon' => 'server2.png', + 'note' => '将证书复制到本机指定路径', + 'tasknote' => '请确保php进程有对证书保存路径的写入权限,宝塔面板需关闭防跨站攻击,如果当前是Docker运行的,则需要做目录映射到宿主机。', + 'inputs' => [], + 'taskinputs' => [ + 'format' => [ + 'name' => '证书类型', + 'type' => 'select', + 'options' => [ + ['value'=>'pem', 'label'=>'PEM格式(Nginx/Apache等)'], + ['value'=>'pfx', 'label'=>'PFX格式(IIS)'], + ], + 'value' => 'pem', + 'required' => true, + ], + 'pem_cert_file' => [ + 'name' => '证书保存路径', + 'type' => 'input', + 'placeholder' => '/path/to/cert.pem', + 'show' => 'format==\'pem\'', + 'required' => true, + ], + 'pem_key_file' => [ + 'name' => '私钥保存路径', + 'type' => 'input', + 'placeholder' => '/path/to/key.pem', + 'show' => 'format==\'pem\'', + 'required' => true, + ], + 'pfx_file' => [ + 'name' => 'PFX证书保存路径', + 'type' => 'input', + 'placeholder' => '/path/to/cert.pfx', + 'show' => 'format==\'pfx\'', + 'required' => true, + ], + 'pfx_pass' => [ + 'name' => 'PFX证书密码', + 'type' => 'input', + 'placeholder' => '留空为不设置密码', + 'show' => 'format==\'pfx\'', + ], + 'cmd' => [ + 'name' => '复制完执行命令', + 'type' => 'textarea', + 'placeholder' => '可留空,每行一条命令,如:service nginx reload', + 'note' => '如需执行命令,php需开放exec函数', + ], + ], + ], + ]; + + public static $class_config = [ + 1 => '自建面板', + 2 => '云服务商', + 3 => '服务器', + ]; + + public static function getList() + { + return self::$deploy_config; + } + + private static function getConfig($aid) + { + $account = Db::name('cert_account')->where('id', $aid)->find(); + if (!$account) return false; + return $account; + } + + public static function getInputs($type, $config = null) + { + $config = $config ? json_decode($config, true) : []; + $inputs = self::$deploy_config[$type]['inputs']; + foreach ($inputs as &$input) { + if (isset($config[$input['name']])) { + $input['value'] = $config[$input['name']]; + } + } + return $inputs; + } + + /** + * @return DeployInterface|bool + */ + public static function getModel($aid) + { + $account = self::getConfig($aid); + if (!$account) return false; + $type = $account['type']; + $class = "\\app\\lib\\deploy\\{$type}"; + if (class_exists($class)) { + $config = json_decode($account['config'], true); + $model = new $class($config); + return $model; + } + return false; + } + + /** + * @return DeployInterface|bool + */ + public static function getModel2($type, $config) + { + $class = "\\app\\lib\\deploy\\{$type}"; + if (class_exists($class)) { + $model = new $class($config); + return $model; + } + return false; + } +} diff --git a/app/lib/DeployInterface.php b/app/lib/DeployInterface.php new file mode 100644 index 0000000..05acf28 --- /dev/null +++ b/app/lib/DeployInterface.php @@ -0,0 +1,12 @@ +generateSecret(); + } + $this->secret = $secret; + } + + public static function create(?string $secret = null) + { + return new self($secret); + } + + public function getSecret(): string + { + return $this->secret; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): void + { + $this->label = $label; + } + + public function getIssuer(): ?string + { + return $this->issuer; + } + + public function setIssuer(string $issuer): void + { + $this->issuer = $issuer; + } + + public function verify(string $otp, ?int $timestamp = null, ?int $window = null): bool + { + $timestamp = $this->getTimestamp($timestamp); + + if (null === $window) { + return $this->compareOTP($this->at($timestamp), $otp); + } + + return $this->verifyOtpWithWindow($otp, $timestamp, $window); + } + + private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): bool + { + $window = abs($window); + + for ($i = 0; $i <= $window; ++$i) { + $next = $i * $this->period + $timestamp; + $previous = -$i * $this->period + $timestamp; + $valid = $this->compareOTP($this->at($next), $otp) || + $this->compareOTP($this->at($previous), $otp); + + if ($valid) { + return true; + } + } + + return false; + } + + public function getProvisioningUri(): string + { + $params = []; + if (30 !== $this->period) { + $params['period'] = $this->period; + } + if (0 !== $this->epoch) { + $params['epoch'] = $this->epoch; + } + $label = $this->getLabel(); + if (null === $label) { + throw new \InvalidArgumentException('The label is not set.'); + } + if ($this->hasColon($label)) { + throw new \InvalidArgumentException('Label must not contain a colon.'); + } + $params['issuer'] = $this->getIssuer(); + $params['secret'] = $this->getSecret(); + $query = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($params)); + return sprintf('otpauth://totp/%s?%s', rawurlencode((null !== $this->getIssuer() ? $this->getIssuer() . ':' : '') . $label), $query); + } + + /** + * The OTP at the specified input. + */ + private function generateOTP(int $input): string + { + $hash = hash_hmac($this->digest, $this->intToByteString($input), $this->base32_decode($this->getSecret()), true); + + $hmac = array_values(unpack('C*', $hash)); + + $offset = ($hmac[\count($hmac) - 1] & 0xF); + $code = ($hmac[$offset + 0] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF); + $otp = $code % (10 ** $this->digits); + + return str_pad((string) $otp, $this->digits, '0', STR_PAD_LEFT); + } + + private function at(int $timestamp): string + { + return $this->generateOTP($this->timecode($timestamp)); + } + + private function timecode(int $timestamp): int + { + return (int) floor(($timestamp - $this->epoch) / $this->period); + } + + private function getTimestamp(?int $timestamp): int + { + $timestamp = $timestamp ?? time(); + if ($timestamp < 0) { + throw new \InvalidArgumentException('Timestamp must be at least 0.'); + } + + return $timestamp; + } + + private function generateSecret(): string + { + return strtoupper($this->base32_encode(random_bytes(20))); + } + + private function base32_encode($data) + { + $dataSize = strlen($data); + $res = ''; + $remainder = 0; + $remainderSize = 0; + + for ($i = 0; $i < $dataSize; $i++) { + $b = ord($data[$i]); + $remainder = ($remainder << 8) | $b; + $remainderSize += 8; + while ($remainderSize > 4) { + $remainderSize -= 5; + $c = $remainder & (31 << $remainderSize); + $c >>= $remainderSize; + $res .= self::$BASE32_ALPHABET[$c]; + } + } + if ($remainderSize > 0) { + $remainder <<= (5 - $remainderSize); + $c = $remainder & 31; + $res .= self::$BASE32_ALPHABET[$c]; + } + + return $res; + } + + private function base32_decode($data) + { + $data = strtolower($data); + $dataSize = strlen($data); + $buf = 0; + $bufSize = 0; + $res = ''; + + for ($i = 0; $i < $dataSize; $i++) { + $c = $data[$i]; + $b = strpos(self::$BASE32_ALPHABET, $c); + if ($b === false) { + throw new \Exception('Encoded string is invalid, it contains unknown char #'.ord($c)); + } + $buf = ($buf << 5) | $b; + $bufSize += 5; + if ($bufSize > 7) { + $bufSize -= 8; + $b = ($buf & (0xff << $bufSize)) >> $bufSize; + $res .= chr($b); + } + } + + return $res; + } + + private function intToByteString(int $int): string + { + $result = []; + while (0 !== $int) { + $result[] = \chr($int & 0xFF); + $int >>= 8; + } + + return str_pad(implode(array_reverse($result)), 8, "\000", STR_PAD_LEFT); + } + + private function compareOTP(string $safe, string $user): bool + { + return hash_equals($safe, $user); + } + + private function hasColon(string $value): bool + { + $colons = [':', '%3A', '%3a']; + foreach ($colons as $colon) { + if (false !== mb_strpos($value, $colon)) { + return true; + } + } + + return false; + } +} diff --git a/app/lib/acme/ACMECert.php b/app/lib/acme/ACMECert.php new file mode 100644 index 0000000..592e36d --- /dev/null +++ b/app/lib/acme/ACMECert.php @@ -0,0 +1,683 @@ +_register($termsOfServiceAgreed, $contacts); + } + + public function registerEAB($termsOfServiceAgreed, $eab_kid, $eab_hmac, $contacts = array()) + { + if (!$this->resources) $this->readDirectory(); + + $protected = array( + 'alg' => 'HS256', + 'kid' => $eab_kid, + 'url' => $this->resources['newAccount'] + ); + $payload = $this->jwk_header['jwk']; + + $protected64 = $this->base64url(json_encode($protected, JSON_UNESCAPED_SLASHES)); + $payload64 = $this->base64url(json_encode($payload, JSON_UNESCAPED_SLASHES)); + + $signature = hash_hmac('sha256', $protected64 . '.' . $payload64, $this->base64url_decode($eab_hmac), true); + + return $this->_register($termsOfServiceAgreed, $contacts, array( + 'externalAccountBinding' => array( + 'protected' => $protected64, + 'payload' => $payload64, + 'signature' => $this->base64url($signature) + ) + )); + } + + private function _register($termsOfServiceAgreed = false, $contacts = array(), $extra = array()) + { + $this->log('Registering account'); + + $ret = $this->request('newAccount', array( + 'termsOfServiceAgreed' => (bool)$termsOfServiceAgreed, + 'contact' => $this->make_contacts_array($contacts) + ) + $extra); + $this->log($ret['code'] == 201 ? 'Account registered' : 'Account already registered'); + return $this->kid_header['kid']; + } + + public function update($contacts = array()) + { + $this->log('Updating account'); + $ret = $this->request($this->getAccountID(), array( + 'contact' => $this->make_contacts_array($contacts) + )); + $this->log('Account updated'); + return $ret['body']; + } + + public function getAccount() + { + $ret = parent::getAccount(); + return $this->kid_header['kid']; + } + + public function setAccount($kid) + { + $this->kid_header['kid'] = $kid; + } + + public function deactivateAccount() + { + $this->log('Deactivating account'); + $ret = $this->deactivate($this->getAccountID()); + $this->log('Account deactivated'); + return $ret; + } + + public function deactivate($url) + { + $this->log('Deactivating resource: ' . $url); + $ret = $this->request($url, array('status' => 'deactivated')); + $this->log('Resource deactivated'); + return $ret['body']; + } + + public function getTermsURL() + { + if (!$this->resources) $this->readDirectory(); + if (!isset($this->resources['meta']['termsOfService'])) { + throw new Exception('Failed to get Terms Of Service URL'); + } + return $this->resources['meta']['termsOfService']; + } + + public function getCAAIdentities() + { + if (!$this->resources) $this->readDirectory(); + if (!isset($this->resources['meta']['caaIdentities'])) { + throw new Exception('Failed to get CAA Identities'); + } + return $this->resources['meta']['caaIdentities']; + } + + public function keyChange($new_account_key_pem) + { // account key roll-over + $this->loadAccountKey($new_account_key_pem); + $account = $this->getAccountID(); + $this->resources = $this->resources; + + $this->log('Account Key Roll-Over'); + + $ret = $this->request( + 'keyChange', + $this->jws_encapsulate('keyChange', array( + 'account' => $account, + 'oldKey' => $this->jwk_header['jwk'] + ), true) + ); + $this->log('Account Key Roll-Over successful'); + + $this->loadAccountKey($new_account_key_pem); + return $ret['body']; + } + + public function revoke($pem) + { + if (false === ($res = openssl_x509_read($pem))) { + throw new Exception('Could not load certificate: ' . $pem . ' (' . $this->get_openssl_error() . ')'); + } + if (false === (openssl_x509_export($res, $certificate))) { + throw new Exception('Could not export certificate: ' . $pem . ' (' . $this->get_openssl_error() . ')'); + } + + $this->log('Revoking certificate'); + $this->request('revokeCert', array( + 'certificate' => $this->base64url($this->pem2der($certificate)) + )); + $this->log('Certificate revoked'); + } + + public function createOrder($domain_config, $settings = array()) + { + $settings = $this->parseSettings($settings); + + $domain_config = array_change_key_case($domain_config, CASE_LOWER); + $domains = array_keys($domain_config); + $authz_deactivated = false; + + // === Order === + $this->log('Creating Order'); + $ret = $this->request('newOrder', $this->makeOrder($domains, $settings)); + $order = $ret['body']; + $order_location = $ret['headers']['location']; + $this->log('Order created: ' . $order_location); + $order['location'] = $order_location; + + // === Authorization === + if ($order['status'] === 'ready' && $settings['authz_reuse']) { + $this->log('All authorizations already valid, skipping validation altogether'); + } else { + $auth_count = count($order['authorizations']); + $challenges = array(); + + foreach ($order['authorizations'] as $idx => $auth_url) { + $this->log('Fetching authorization ' . ($idx + 1) . ' of ' . $auth_count); + $ret = $this->request($auth_url, ''); + $authorization = $ret['body']; + + // wildcard authorization identifiers have no leading *. + $domain = ( // get domain and add leading *. if wildcard is used + isset($authorization['wildcard']) && + $authorization['wildcard'] ? + '*.' : '' + ) . $authorization['identifier']['value']; + + if ($authorization['status'] === 'valid') { + if ($settings['authz_reuse']) { + $this->log('Authorization of ' . $domain . ' already valid, skipping validation'); + } else { + $this->log('Authorization of ' . $domain . ' already valid, deactivating authorization'); + $this->deactivate($auth_url); + $authz_deactivated = true; + } + continue; + } + + if(!isset($domain_config[$domain])) { + $this->log('Domain ' . $domain . ' not found in domain_config'); + continue; + } + + $config = $domain_config[$domain]; + $type = $config['challenge']; + + $challenge = $this->parse_challenges($authorization, $type, $challenge_url); + + $opts = array( + 'domain' => $domain, + 'type' => $type, + 'auth_url' => $auth_url, + 'challenge_url' => $challenge_url + ); + list($opts['key'], $opts['value']) = $challenge; + + $challenges[] = $opts; + } + + if ($authz_deactivated) { + $this->log('Restarting Order after deactivating already valid authorizations'); + $settings['authz_reuse'] = true; + return $this->createOrder($domain_config, $settings); + } + + $order['challenges'] = $challenges; + } + return $order; + } + + public function authOrder($order) + { + if ($order['status'] != 'ready' && empty($order['challenges'])) { + throw new Exception('No challenges available'); + } + + // === Challenge === + if (!empty($order['challenges'])){ + foreach ($order['challenges'] as $opts) { + $this->log('Notifying server for validation of ' . $opts['domain']); + $this->request($opts['challenge_url'], new stdClass); + + $this->log('Waiting for server challenge validation'); + sleep(1); + + if (!$this->poll('pending', $opts['auth_url'], $body)) { + $this->log('Validation failed: ' . $opts['domain']); + + $error = $body['challenges'][0]['error']; + throw $this->create_ACME_Exception( + $error['type'], + 'Challenge validation failed: ' . $error['detail'] + ); + } else { + $this->log('Validation successful: ' . $opts['domain']); + } + } + } + } + + public function finalizeOrder($domains, $order, $pem) + { + // autodetect if Private Key or CSR is used + if ($key = openssl_pkey_get_private($pem)) { // Private Key detected + if (PHP_MAJOR_VERSION < 8) openssl_free_key($key); + $this->log('Generating CSR'); + $csr = $this->generateCSR($pem, $domains); + } elseif (openssl_csr_get_subject($pem)) { // CSR detected + $this->log('Using provided CSR'); + if (0 === strpos($pem, 'file://')) { + $csr = file_get_contents(substr($pem, 7)); + if (false === $csr) { + throw new Exception('Failed to read CSR from ' . $pem . ' (' . $this->get_openssl_error() . ')'); + } + } else { + $csr = $pem; + } + } else { + throw new Exception('Could not load Private Key or CSR (' . $this->get_openssl_error() . '): ' . $pem); + } + + $this->log('Finalizing Order'); + + $ret = $this->request($order['finalize'], array( + 'csr' => $this->base64url($this->pem2der($csr)) + )); + $ret = $ret['body']; + + if (isset($ret['certificate'])) { + return $this->request_certificate($ret); + } + + if ($this->poll('processing', $order['location'], $ret)) { + return $this->request_certificate($ret); + } + + throw new Exception('Order failed'); + } + + public function finalizeOrders($domains, $order, $pem) + { + $default_chain = $this->finalizeOrder($domains, $order, $pem); + + $out = array(); + $out[$this->getTopIssuerCN($default_chain)] = $default_chain; + + foreach ($this->alternate_chains as $link) { + $chain = $this->request_certificate(array('certificate' => $link), true); + $out[$this->getTopIssuerCN($chain)] = $chain; + } + + $this->log('Received ' . count($out) . ' chain(s): ' . implode(', ', array_keys($out))); + return $out; + } + + public function generateCSR($domain_key_pem, $domains) + { + if (false === ($domain_key = openssl_pkey_get_private($domain_key_pem))) { + throw new Exception('Could not load domain key: ' . $domain_key_pem . ' (' . $this->get_openssl_error() . ')'); + } + + $fn = $this->tmp_ssl_cnf($domains); + $cn = reset($domains); + $dn = array(); + if (strlen($cn) <= 64) { + $dn['commonName'] = $cn; + } + $csr = openssl_csr_new($dn, $domain_key, array( + 'config' => $fn, + 'req_extensions' => 'SAN', + 'digest_alg' => 'sha512' + )); + unlink($fn); + if (PHP_MAJOR_VERSION < 8) openssl_free_key($domain_key); + + if (false === $csr) { + throw new Exception('Could not generate CSR ! (' . $this->get_openssl_error() . ')'); + } + if (false === openssl_csr_export($csr, $out)) { + throw new Exception('Could not export CSR ! (' . $this->get_openssl_error() . ')'); + } + + return $out; + } + + private function generateKey($opts) + { + $fn = $this->tmp_ssl_cnf(); + $config = array('config' => $fn) + $opts; + if (false === ($key = openssl_pkey_new($config))) { + throw new Exception('Could not generate new private key ! (' . $this->get_openssl_error() . ')'); + } + if (false === openssl_pkey_export($key, $pem, null, $config)) { + throw new Exception('Could not export private key ! (' . $this->get_openssl_error() . ')'); + } + unlink($fn); + if (PHP_MAJOR_VERSION < 8) openssl_free_key($key); + return $pem; + } + + public function generateRSAKey($bits = 2048) + { + return $this->generateKey(array( + 'private_key_bits' => (int)$bits, + 'private_key_type' => OPENSSL_KEYTYPE_RSA + )); + } + + public function generateECKey($curve_name = '384') + { + if (version_compare(PHP_VERSION, '7.1.0') < 0) throw new Exception('PHP >= 7.1.0 required for EC keys !'); + $map = array('256' => 'prime256v1', '384' => 'secp384r1', '521' => 'secp521r1'); + if (isset($map[$curve_name])) $curve_name = $map[$curve_name]; + return $this->generateKey(array( + 'curve_name' => $curve_name, + 'private_key_type' => OPENSSL_KEYTYPE_EC + )); + } + + public function parseCertificate($cert_pem) + { + if (false === ($ret = openssl_x509_read($cert_pem))) { + throw new Exception('Could not load certificate: ' . $cert_pem . ' (' . $this->get_openssl_error() . ')'); + } + if (!is_array($ret = openssl_x509_parse($ret, true))) { + throw new Exception('Could not parse certificate (' . $this->get_openssl_error() . ')'); + } + return $ret; + } + + public function getSAN($pem) + { + $ret = $this->parseCertificate($pem); + if (!isset($ret['extensions']['subjectAltName'])) { + throw new Exception('No Subject Alternative Name (SAN) found in certificate'); + } + $out = array(); + foreach (explode(',', $ret['extensions']['subjectAltName']) as $line) { + list($type, $name) = array_map('trim', explode(':', $line)); + if ($type === 'DNS') { + $out[] = $name; + } + } + return $out; + } + + public function getRemainingDays($cert_pem) + { + $ret = $this->parseCertificate($cert_pem); + return ($ret['validTo_time_t'] - time()) / 86400; + } + + public function getRemainingPercent($cert_pem) + { + $ret = $this->parseCertificate($cert_pem); + $total = $ret['validTo_time_t'] - $ret['validFrom_time_t']; + $used = time() - $ret['validFrom_time_t']; + return (1 - max(0, min(1, $used / $total))) * 100; + } + + public function generateALPNCertificate($domain_key_pem, $domain, $token) + { + $domains = array($domain); + $csr = $this->generateCSR($domain_key_pem, $domains); + + $fn = $this->tmp_ssl_cnf($domains, '1.3.6.1.5.5.7.1.31=critical,DER:0420' . $token . "\n"); + $config = array( + 'config' => $fn, + 'x509_extensions' => 'SAN', + 'digest_alg' => 'sha512' + ); + $cert = openssl_csr_sign($csr, null, $domain_key_pem, 1, $config); + unlink($fn); + if (false === $cert) { + throw new Exception('Could not generate self signed certificate ! (' . $this->get_openssl_error() . ')'); + } + if (false === openssl_x509_export($cert, $out)) { + throw new Exception('Could not export self signed certificate ! (' . $this->get_openssl_error() . ')'); + } + return $out; + } + + public function getARI($pem, &$ari_cert_id = null) + { + $ari_cert_id = null; + $id = $this->getARICertID($pem); + + if (!$this->resources) $this->readDirectory(); + if (!isset($this->resources['renewalInfo'])) throw new Exception('ARI not supported'); + + $ret = $this->http_request($this->resources['renewalInfo'] . '/' . $id); + + if (!is_array($ret['body']['suggestedWindow'])) throw new Exception('ARI suggestedWindow not present'); + + $sw = &$ret['body']['suggestedWindow']; + + if (!isset($sw['start'])) throw new Exception('ARI suggestedWindow start not present'); + if (!isset($sw['end'])) throw new Exception('ARI suggestedWindow end not present'); + + $sw = array_map(array($this, 'parseDate'), $sw); + + $ari_cert_id = $id; + return $ret['body']; + } + + private function getARICertID($pem) + { + if (version_compare(PHP_VERSION, '7.1.2', '<')) { + throw new Exception('PHP Version >= 7.1.2 required for ARI'); // serialNumberHex - https://github.com/php/php-src/pull/1755 + } + $ret = $this->parseCertificate($pem); + + if (!isset($ret['extensions']['authorityKeyIdentifier'])) { + throw new Exception('authorityKeyIdentifier missing'); + } + $aki = hex2bin(str_replace(':', '', substr(trim($ret['extensions']['authorityKeyIdentifier']), 6))); + if (!$aki) throw new Exception('Failed to parse authorityKeyIdentifier'); + + if (!isset($ret['serialNumberHex'])) { + throw new Exception('serialNumberHex missing'); + } + $ser = hex2bin(trim($ret['serialNumberHex'])); + if (!$ser) throw new Exception('Failed to parse serialNumberHex'); + + return $this->base64url($aki) . '.' . $this->base64url($ser); + } + + private function parseDate($str) + { + $ret = strtotime(preg_replace('/(\.\d\d)\d+/', '$1', $str)); + if ($ret === false) throw new Exception('Failed to parse date: ' . $str); + return $ret; + } + + private function parseSettings($opts) + { + // authz_reuse: backwards compatibility to ACMECert v3.1.2 or older + if (!is_array($opts)) $opts = array('authz_reuse' => (bool)$opts); + if (!isset($opts['authz_reuse'])) $opts['authz_reuse'] = true; + + $diff = array_diff_key( + $opts, + array_flip(array('authz_reuse', 'notAfter', 'notBefore', 'replaces')) + ); + + if (!empty($diff)) { + throw new Exception('getCertificateChain(s): Invalid option "' . key($diff) . '"'); + } + + return $opts; + } + + private function setRFC3339Date(&$out, $key, $opts) + { + if (isset($opts[$key])) { + $out[$key] = is_string($opts[$key]) ? + $opts[$key] : + date(DATE_RFC3339, $opts[$key]); + } + } + + private function makeOrder($domains, $opts) + { + $order = array( + 'identifiers' => array_map( + function ($domain) { + return array('type' => 'dns', 'value' => $domain); + }, + $domains + ) + ); + $this->setRFC3339Date($order, 'notAfter', $opts); + $this->setRFC3339Date($order, 'notBefore', $opts); + + if (isset($opts['replaces'])) { // ARI + $order['replaces'] = $opts['replaces']; + $this->log('Replacing Certificate: ' . $opts['replaces']); + } + + return $order; + } + + private function parse_challenges($authorization, $type, &$url) + { + foreach ($authorization['challenges'] as $challenge) { + if ($challenge['type'] != $type) continue; + + $url = $challenge['url']; + + switch ($challenge['type']) { + case 'dns-01': + return array( + '_acme-challenge.' . $authorization['identifier']['value'], + $this->base64url(hash('sha256', $this->keyAuthorization($challenge['token']), true)) + ); + break; + case 'http-01': + return array( + '/.well-known/acme-challenge/' . $challenge['token'], + $this->keyAuthorization($challenge['token']) + ); + break; + case 'tls-alpn-01': + return array(null, hash('sha256', $this->keyAuthorization($challenge['token']))); + break; + } + } + throw new Exception( + 'Challenge type: "' . $type . '" not available, for this challenge use ' . + implode(' or ', array_map( + function ($a) { + return '"' . $a['type'] . '"'; + }, + $authorization['challenges'] + )) + ); + } + + private function poll($initial, $type, &$ret) + { + $max_tries = 10; // ~ 5 minutes + for ($i = 0; $i < $max_tries; $i++) { + $ret = $this->request($type); + $ret = $ret['body']; + if ($ret['status'] !== $initial) return $ret['status'] === 'valid'; + $s = pow(2, min($i, 6)); + if ($i !== $max_tries - 1) { + $this->log('Retrying in ' . ($s) . 's'); + sleep($s); + } + } + throw new Exception('Aborted after ' . $max_tries . ' tries'); + } + + private function request_certificate($ret, $alternate = false) + { + $this->log('Requesting ' . ($alternate ? 'alternate' : 'default') . ' certificate-chain'); + $ret = $this->request($ret['certificate'], ''); + if ($ret['headers']['content-type'] !== 'application/pem-certificate-chain') { + throw new Exception('Unexpected content-type: ' . $ret['headers']['content-type']); + } + + $chain = array(); + foreach ($this->splitChain($ret['body']) as $cert) { + $info = $this->parseCertificate($cert); + $chain[] = '[' . $info['issuer']['CN'] . ']'; + } + + if (!$alternate) { + if (isset($ret['headers']['link']['alternate'])) { + $this->alternate_chains = $ret['headers']['link']['alternate']; + } else { + $this->alternate_chains = array(); + } + } + + $this->log(($alternate ? 'Alternate' : 'Default') . ' certificate-chain retrieved: ' . implode(' -> ', array_reverse($chain, true))); + return $ret['body']; + } + + private function tmp_ssl_cnf($domains = null, $extension = '') + { + if (false === ($fn = tempnam(sys_get_temp_dir(), "CNF_"))) { + throw new Exception('Failed to create temp file !'); + } + if (false === @file_put_contents( + $fn, + 'HOME = .' . "\n" . + 'RANDFILE=$ENV::HOME/.rnd' . "\n" . + '[v3_ca]' . "\n" . + '[req]' . "\n" . + 'default_bits=2048' . "\n" . + ($domains ? + 'distinguished_name=req_distinguished_name' . "\n" . + '[req_distinguished_name]' . "\n" . + '[v3_req]' . "\n" . + '[SAN]' . "\n" . + 'subjectAltName=' . + implode(',', array_map(function ($domain) { + return 'DNS:' . $domain; + }, $domains)) . "\n" + : + '' + ) . $extension + )) { + throw new Exception('Failed to write tmp file: ' . $fn); + } + return $fn; + } + + private function pem2der($pem) + { + return base64_decode(implode('', array_slice( + array_map('trim', explode("\n", trim($pem))), + 1, + -1 + ))); + } + + private function make_contacts_array($contacts) + { + if (!is_array($contacts)) { + $contacts = $contacts ? array($contacts) : array(); + } + return array_map(function ($contact) { + return 'mailto:' . $contact; + }, $contacts); + } + + private function getTopIssuerCN($chain) + { + $tmp = $this->splitChain($chain); + $ret = $this->parseCertificate(end($tmp)); + return $ret['issuer']['CN']; + } + + public function splitChain($chain) + { + $delim = '-----END CERTIFICATE-----'; + return array_map(function ($item) use ($delim) { + return trim($item . $delim); + }, array_filter(explode($delim, $chain), function ($item) { + return strpos($item, '-----BEGIN CERTIFICATE-----') !== false; + })); + } +} diff --git a/app/lib/acme/ACME_Exception.php b/app/lib/acme/ACME_Exception.php new file mode 100644 index 0000000..ba251b5 --- /dev/null +++ b/app/lib/acme/ACME_Exception.php @@ -0,0 +1,24 @@ +type = $type; + $this->subproblems = $subproblems; + parent::__construct($detail); + } + function getType() + { + return $this->type; + } + function getSubproblems() + { + return $this->subproblems; + } +} diff --git a/app/lib/acme/ACMEv2.php b/app/lib/acme/ACMEv2.php new file mode 100644 index 0000000..b1fb5b8 --- /dev/null +++ b/app/lib/acme/ACMEv2.php @@ -0,0 +1,428 @@ +directory = $directory; + $this->proxy = $proxy; + } + + public function __destruct() + { + if (PHP_MAJOR_VERSION < 8 && $this->account_key) openssl_pkey_free($this->account_key); + if ($this->ch) curl_close($this->ch); + } + + public function loadAccountKey($account_key_pem) + { + if (PHP_MAJOR_VERSION < 8 && $this->account_key) openssl_pkey_free($this->account_key); + if (false === ($this->account_key = openssl_pkey_get_private($account_key_pem))) { + throw new Exception('Could not load account key: ' . $account_key_pem . ' (' . $this->get_openssl_error() . ')'); + } + + if (false === ($details = openssl_pkey_get_details($this->account_key))) { + throw new Exception('Could not get account key details: ' . $account_key_pem . ' (' . $this->get_openssl_error() . ')'); + } + + $this->bits = $details['bits']; + switch ($details['type']) { + case OPENSSL_KEYTYPE_EC: + if (version_compare(PHP_VERSION, '7.1.0') < 0) throw new Exception('PHP >= 7.1.0 required for EC keys !'); + $this->sha_bits = ($this->bits == 521 ? 512 : $this->bits); + $this->jwk_header = array( // JOSE Header - RFC7515 + 'alg' => 'ES' . $this->sha_bits, + 'jwk' => array( // JSON Web Key + 'crv' => 'P-' . $details['bits'], + 'kty' => 'EC', + 'x' => $this->base64url(str_pad($details['ec']['x'], ceil($this->bits / 8), "\x00", STR_PAD_LEFT)), + 'y' => $this->base64url(str_pad($details['ec']['y'], ceil($this->bits / 8), "\x00", STR_PAD_LEFT)) + ) + ); + break; + case OPENSSL_KEYTYPE_RSA: + $this->sha_bits = 256; + $this->jwk_header = array( // JOSE Header - RFC7515 + 'alg' => 'RS256', + 'jwk' => array( // JSON Web Key + 'e' => $this->base64url($details['rsa']['e']), // public exponent + 'kty' => 'RSA', + 'n' => $this->base64url($details['rsa']['n']) // public modulus + ) + ); + break; + default: + throw new Exception('Unsupported key type! Must be RSA or EC key.'); + break; + } + + $this->kid_header = array( + 'alg' => $this->jwk_header['alg'], + 'kid' => null + ); + + $this->thumbprint = $this->base64url( // JSON Web Key (JWK) Thumbprint - RFC7638 + hash( + 'sha256', + json_encode($this->jwk_header['jwk']), + true + ) + ); + } + + public function getAccountID() + { + if (!$this->kid_header['kid']) self::getAccount(); + return $this->kid_header['kid']; + } + + public function setLogger($value = true) + { + switch (true) { + case is_bool($value): + break; + case is_callable($value): + break; + default: + throw new Exception('setLogger: invalid value provided'); + break; + } + $this->logger = $value; + } + + public function log($txt) + { + switch (true) { + case $this->logger === true: + error_log($txt); + break; + case $this->logger === false: + break; + default: + $fn = $this->logger; + $fn($txt); + break; + } + } + + protected function create_ACME_Exception($type, $detail, $subproblems = array()) + { + $this->log('ACME_Exception: ' . $detail . ' (' . $type . ')'); + return new ACME_Exception($type, $detail, $subproblems); + } + + protected function get_openssl_error() + { + $out = array(); + $arr = error_get_last(); + if (is_array($arr)) { + $out[] = $arr['message']; + } + $out[] = openssl_error_string(); + return implode(' | ', $out); + } + + protected function getAccount() + { + $this->log('Getting account info'); + $ret = $this->request('newAccount', array('onlyReturnExisting' => true)); + $this->log('Account info retrieved'); + return $ret; + } + + protected function keyAuthorization($token) + { + return $token . '.' . $this->thumbprint; + } + + protected function readDirectory() + { + $this->log('Initializing ACME v2 environment: ' . $this->directory); + $ret = $this->http_request($this->directory); // Read ACME Directory + if ( + !is_array($ret['body']) || + !empty(array_diff_key( + array_flip(array('newNonce', 'newAccount', 'newOrder')), + $ret['body'] + )) + ) { + throw new Exception('Failed to read directory: ' . $this->directory); + } + $this->resources = $ret['body']; // store resources for later use + $this->log('Initialized'); + } + + protected function request($type, $payload = '', $retry = false) + { + if (!$this->jwk_header) { + throw new Exception('use loadAccountKey to load an account key'); + } + + if (!$this->resources) $this->readDirectory(); + + if (0 === stripos($type, 'http')) { + $this->resources['_tmp'] = $type; + $type = '_tmp'; + } + + try { + $ret = $this->http_request($this->resources[$type], json_encode( + $this->jws_encapsulate($type, $payload) + )); + } catch (ACME_Exception $e) { // retry previous request once, if replay-nonce expired/failed + if (!$retry && $e->getType() === 'urn:ietf:params:acme:error:badNonce') { + $this->log('Replay-Nonce expired, retrying previous request'); + return $this->request($type, $payload, true); + } + if (!$retry && $e->getType() === 'urn:ietf:params:acme:error:rateLimited' && $this->delay_until !== null) { + return $this->request($type, $payload, true); + } + throw $e; // rethrow all other exceptions + } + + if (!$this->kid_header['kid'] && $type === 'newAccount') { + $this->kid_header['kid'] = $ret['headers']['location']; + $this->log('AccountID: ' . $this->kid_header['kid']); + } + + return $ret; + } + + protected function jws_encapsulate($type, $payload, $is_inner_jws = false) + { // RFC7515 + if ($type === 'newAccount' || $is_inner_jws) { + $protected = $this->jwk_header; + } else { + $this->getAccountID(); + $protected = $this->kid_header; + } + + if (!$is_inner_jws) { + if (!$this->nonce) { + $ret = $this->http_request($this->resources['newNonce'], false); + } + $protected['nonce'] = $this->nonce; + $this->nonce = null; + } + + if (!isset($this->resources[$type])) { + throw new Exception('Resource "' . $type . '" not available.'); + } + + $protected['url'] = $this->resources[$type]; + + $protected64 = $this->base64url(json_encode($protected, JSON_UNESCAPED_SLASHES)); + $payload64 = $this->base64url(is_string($payload) ? $payload : json_encode($payload, JSON_UNESCAPED_SLASHES)); + + if (false === openssl_sign( + $protected64 . '.' . $payload64, + $signature, + $this->account_key, + 'SHA' . $this->sha_bits + )) { + throw new Exception('Failed to sign payload !' . ' (' . $this->get_openssl_error() . ')'); + } + + return array( + 'protected' => $protected64, + 'payload' => $payload64, + 'signature' => $this->base64url($this->jwk_header['alg'][0] == 'R' ? $signature : $this->asn2signature($signature, ceil($this->bits / 8))) + ); + } + + private function asn2signature($asn, $pad_len) + { + if ($asn[0] !== "\x30") throw new Exception('ASN.1 SEQUENCE not found !'); + $asn = substr($asn, $asn[1] === "\x81" ? 3 : 2); + if ($asn[0] !== "\x02") throw new Exception('ASN.1 INTEGER 1 not found !'); + $R = ltrim(substr($asn, 2, ord($asn[1])), "\x00"); + $asn = substr($asn, ord($asn[1]) + 2); + if ($asn[0] !== "\x02") throw new Exception('ASN.1 INTEGER 2 not found !'); + $S = ltrim(substr($asn, 2, ord($asn[1])), "\x00"); + return str_pad($R, $pad_len, "\x00", STR_PAD_LEFT) . str_pad($S, $pad_len, "\x00", STR_PAD_LEFT); + } + + protected function base64url($data) + { // RFC7515 - Appendix C + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + + protected function base64url_decode($data) + { + return base64_decode(strtr($data, '-_', '+/')); + } + + private function json_decode($str) + { + $ret = json_decode($str, true); + if ($ret === null) { + throw new Exception('Could not parse JSON: ' . $str); + } + return $ret; + } + + protected function http_request($url, $data = null) + { + if ($this->ch === null) { + $this->ch = curl_init(); + } + + if ($this->delay_until !== null) { + $delta = $this->delay_until - time(); + if ($delta > 0) { + $this->log('Delaying ' . $delta . 's (rate limit)'); + sleep($delta); + } + $this->delay_until = null; + } + + $method = $data === false ? 'HEAD' : ($data === null ? 'GET' : 'POST'); + $user_agent = 'ACMECert v3.4.0 (+https://github.com/skoerfgen/ACMECert)'; + $header = ($data === null || $data === false) ? array() : array('Content-Type: application/jose+json'); + + $headers = array(); + curl_setopt_array($this->ch, array( + CURLOPT_URL => $url, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_NOBODY => $data === false, + CURLOPT_USERAGENT => $user_agent, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => $header, + CURLOPT_POSTFIELDS => $data, + CURLOPT_HEADERFUNCTION => static function ($ch, $header) use (&$headers) { + $headers[] = $header; + return strlen($header); + } + )); + + if ($this->proxy) { + $proxy_server = config_get('proxy_server'); + $proxy_port = intval(config_get('proxy_port')); + $proxy_userpwd = config_get('proxy_user').':'.config_get('proxy_pwd'); + $proxy_type = config_get('proxy_type'); + if ($proxy_type == 'https') { + $proxy_type = CURLPROXY_HTTPS; + } elseif ($proxy_type == 'sock4') { + $proxy_type = CURLPROXY_SOCKS4; + } elseif ($proxy_type == 'sock5') { + $proxy_type = CURLPROXY_SOCKS5; + } else { + $proxy_type = CURLPROXY_HTTP; + } + curl_setopt($this->ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC); + curl_setopt($this->ch, CURLOPT_PROXY, $proxy_server); + curl_setopt($this->ch, CURLOPT_PROXYPORT, $proxy_port); + if ($proxy_userpwd != ':') { + curl_setopt($this->ch, CURLOPT_PROXYUSERPWD, $proxy_userpwd); + } + curl_setopt($this->ch, CURLOPT_PROXYTYPE, $proxy_type); + } + + $took = microtime(true); + $body = curl_exec($this->ch); + $took = round(microtime(true) - $took, 2) . 's'; + if ($body === false) throw new Exception('HTTP Request Error: ' . curl_error($this->ch)); + + $headers = array_reduce( // parse http response headers into array + array_filter($headers, function ($item) { + return trim($item) != ''; + }), + function ($carry, $item) use (&$code) { + $parts = explode(':', $item, 2); + if (count($parts) === 1) { + list(, $code) = explode(' ', trim($item), 3); + $carry = array(); + } else { + list($k, $v) = $parts; + $k = strtolower(trim($k)); + switch ($k) { + case 'link': + if (preg_match('/<(.*)>\s*;\s*rel=\"(.*)\"/', $v, $matches)) { + $carry[$k][$matches[2]][] = trim($matches[1]); + } + break; + case 'content-type': + list($v) = explode(';', $v, 2); + default: + $carry[$k] = trim($v); + break; + } + } + return $carry; + }, + array() + ); + $this->log(' ' . $url . ' [' . $code . '] (' . $took . ')'); + + if (!empty($headers['replay-nonce'])) $this->nonce = $headers['replay-nonce']; + + if (isset($headers['retry-after'])) { + if (is_numeric($headers['retry-after'])) { + $this->delay_until = time() + ceil($headers['retry-after']); + } else { + $this->delay_until = strtotime($headers['retry-after']); + } + $tmp = $this->delay_until - time(); + // ignore delay if not in range 1s..5min + if ($tmp > 300 || $tmp < 1) $this->delay_until = null; + } + + if (!empty($headers['content-type'])) { + switch ($headers['content-type']) { + case 'application/json': + if ($code[0] == '2') { // on non 2xx response: fall through to problem+json case + $body = $this->json_decode($body); + if (isset($body['error']) && !(isset($body['status']) && $body['status'] === 'valid')) { + $this->handleError($body['error']); + } + break; + } + case 'application/problem+json': + $body = $this->json_decode($body); + $this->handleError($body); + break; + } + } + + if ($code[0] != '2') { + throw new Exception('Invalid HTTP-Status-Code received: ' . $code . ': ' . print_r($body, true)); + } + + $ret = array( + 'code' => $code, + 'headers' => $headers, + 'body' => $body + ); + + return $ret; + } + + private function handleError($error) + { + throw $this->create_ACME_Exception( + $error['type'], + $error['detail'], + array_map(function ($subproblem) { + return $this->create_ACME_Exception( + $subproblem['type'], + (isset($subproblem['identifier']['value']) ? + '"' . $subproblem['identifier']['value'] . '": ' : + '' + ) . $subproblem['detail'] + ); + }, isset($error['subproblems']) ? $error['subproblems'] : array()) + ); + } +} diff --git a/app/lib/cert/aliyun.php b/app/lib/cert/aliyun.php new file mode 100644 index 0000000..7aff3c1 --- /dev/null +++ b/app/lib/cert/aliyun.php @@ -0,0 +1,167 @@ +AccessKeyId = $config['AccessKeyId']; + $this->AccessKeySecret = $config['AccessKeySecret']; + $this->client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $this->Endpoint, $this->Version); + $this->config = $config; + } + + public function register() + { + if (empty($this->AccessKeyId) || empty($this->AccessKeySecret) || empty($this->config['username']) || empty($this->config['phone']) || empty($this->config['email'])) throw new Exception('必填参数不能为空'); + $param = ['Action' => 'ListUserCertificateOrder']; + $this->request($param, true); + return true; + } + + public function buyCert($domainList, &$order) + { + $param = ['Action' => 'DescribePackageState', 'ProductCode' => 'digicert-free-1-free']; + $data = $this->request($param, true); + if (!isset($data['TotalCount']) || $data['TotalCount'] == 0) throw new Exception('没有可用的免费证书资源包'); + $this->log('证书资源包总数量:' . $data['TotalCount'] . ',已使用数量:' . $data['UsedCount']); + } + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + $domain = $domainList[0]; + $param = [ + 'Action' => 'CreateCertificateRequest', + 'ProductCode' => 'digicert-free-1-free', + 'Username' => $this->config['username'], + 'Phone' => $this->config['phone'], + 'Email' => $this->config['email'], + 'Domain' => $domain, + 'ValidateType' => 'DNS' + ]; + $data = $this->request($param, true); + if (empty($data['OrderId'])) throw new Exception('证书申请失败,OrderId为空'); + $order['OrderId'] = $data['OrderId']; + + sleep(3); + + $param = [ + 'Action' => 'DescribeCertificateState', + 'OrderId' => $order['OrderId'], + ]; + $data = $this->request($param, true); + + $dnsList = []; + if ($data['Type'] == 'domain_verify') { + $mainDomain = getMainDomain($domain); + $name = str_replace('.' . $mainDomain, '', $data['RecordDomain']); + $dnsList[$mainDomain][] = ['name' => $name, 'type' => $data['RecordType'], 'value' => $data['RecordValue']]; + } + + return $dnsList; + } + + public function authOrder($domainList, $order) {} + + public function getAuthStatus($domainList, $order) + { + $param = [ + 'Action' => 'DescribeCertificateState', + 'OrderId' => $order['OrderId'], + ]; + $data = $this->request($param, true); + if ($data['Type'] == 'certificate') { + return true; + } elseif ($data['Type'] == 'verify_fail') { + throw new Exception('证书审核失败'); + } else { + return false; + } + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + $param = [ + 'Action' => 'DescribeCertificateState', + 'OrderId' => $order['OrderId'], + ]; + $data = $this->request($param, true); + $fullchain = $data['Certificate']; + $private_key = $data['PrivateKey']; + if (empty($fullchain) || empty($private_key)) throw new Exception('证书内容获取失败'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $certInfo['issuer']['CN'], 'subject' => $certInfo['subject']['CN'], 'validFrom' => $certInfo['validFrom_time_t'], 'validTo' => $certInfo['validTo_time_t']]; + } + + public function revoke($order, $pem) + { + $param = [ + 'Action' => 'CancelCertificateForPackageRequest', + 'OrderId' => $order['OrderId'], + ]; + $this->request($param); + } + + public function cancel($order) + { + $param = [ + 'Action' => 'DescribeCertificateState', + 'OrderId' => $order['OrderId'], + ]; + $data = $this->request($param, true); + if ($data['Type'] == 'domain_verify' || $data['Type'] == 'process') { + $param = [ + 'Action' => 'CancelOrderRequest', + 'OrderId' => $order['OrderId'], + ]; + $this->request($param); + usleep(500000); + } + if ($data['Type'] == 'domain_verify' || $data['Type'] == 'process' || $data['Type'] == 'payed' || $data['Type'] == 'verify_fail') { + $param = [ + 'Action' => 'DeleteCertificateRequest', + 'OrderId' => $order['OrderId'], + ]; + $this->request($param); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($param, $returnData = false) + { + $this->log('Request:' . json_encode($param, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + $result = $this->client->request($param); + $response = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if (!strpos($response, '"Type":"certificate"')) { + $this->log('Response:' . $response); + } + return $returnData ? $result : true; + } +} diff --git a/app/lib/cert/customacme.php b/app/lib/cert/customacme.php new file mode 100644 index 0000000..5581391 --- /dev/null +++ b/app/lib/cert/customacme.php @@ -0,0 +1,114 @@ +config = $config; + $this->ac = new ACMECert($config['directory'], $config['proxy'] == 1); + if ($ext) { + $this->ext = $ext; + $this->ac->loadAccountKey($ext['key']); + $this->ac->setAccount($ext['kid']); + } + } + + public function register() + { + if (empty($this->config['directory'])) throw new Exception('ACME地址不能为空'); + if (empty($this->config['email'])) throw new Exception('邮件地址不能为空'); + + if (!empty($this->ext['key'])) { + if (!empty($this->config['kid']) && !empty($this->config['key'])) { + $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); + } else { + $kid = $this->ac->register(true, $this->config['email']); + } + return ['kid' => $kid, 'key' => $this->ext['key']]; + } + + $key = $this->ac->generateRSAKey(2048); + $this->ac->loadAccountKey($key); + if (!empty($this->config['kid']) && !empty($this->config['key'])) { + $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); + } else { + $kid = $this->ac->register(true, $this->config['email']); + } + return ['kid' => $kid, 'key' => $key]; + } + + public function buyCert($domainList, &$order) {} + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + $domain_config = []; + foreach ($domainList as $domain) { + if (empty($domain)) continue; + $domain_config[$domain] = ['challenge' => 'dns-01']; + } + if (empty($domain_config)) throw new Exception('域名列表不能为空'); + + $order = $this->ac->createOrder($domain_config); + + $dnsList = []; + if (!empty($order['challenges'])) { + foreach ($order['challenges'] as $opts) { + $mainDomain = getMainDomain($opts['domain']); + $name = str_replace('.' . $mainDomain, '', $opts['key']); + $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; + } + } + + return $dnsList; + } + + public function authOrder($domainList, $order) + { + $this->ac->authOrder($order); + } + + public function getAuthStatus($domainList, $order) + { + return true; + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + + if ($keytype == 'ECC') { + if (empty($keysize)) $keysize = '384'; + $private_key = $this->ac->generateECKey($keysize); + } else { + if (empty($keysize)) $keysize = '2048'; + $private_key = $this->ac->generateRSAKey($keysize); + } + $fullchain = $this->ac->finalizeOrder($domainList, $order, $private_key); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $certInfo['issuer']['CN'], 'subject' => $certInfo['subject']['CN'], 'validFrom' => $certInfo['validFrom_time_t'], 'validTo' => $certInfo['validTo_time_t']]; + } + + public function revoke($order, $pem) + { + $this->ac->revoke($pem); + } + + public function cancel($order) {} + + public function setLogger($func) + { + $this->ac->setLogger($func); + } +} diff --git a/app/lib/cert/google.php b/app/lib/cert/google.php new file mode 100644 index 0000000..8f10384 --- /dev/null +++ b/app/lib/cert/google.php @@ -0,0 +1,118 @@ + 'https://dv.acme-v02.api.pki.goog/directory', + 'staging' => 'https://dv.acme-v02.test-api.pki.goog/directory' + ); + private $ac; + private $config; + private $ext; + + public function __construct($config, $ext = null) + { + $this->config = $config; + if (empty($config['mode'])) $config['mode'] = 'live'; + $this->ac = new ACMECert($this->directories[$config['mode']], $config['proxy']==1); + if ($ext) { + $this->ext = $ext; + $this->ac->loadAccountKey($ext['key']); + $this->ac->setAccount($ext['kid']); + } + } + + public function register() + { + if (empty($this->config['email'])) throw new Exception('邮件地址不能为空'); + if (empty($this->config['kid']) || empty($this->config['key'])) throw new Exception('必填参数不能为空'); + + if (!empty($this->ext['key'])) { + $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); + return ['kid' => $kid, 'key' => $this->ext['key']]; + } + + $key = $this->ac->generateRSAKey(2048); + $this->ac->loadAccountKey($key); + $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); + return ['kid' => $kid, 'key' => $key]; + } + + public function buyCert($domainList, &$order) + { + } + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + $domain_config = []; + foreach ($domainList as $domain) { + if (empty($domain)) continue; + $domain_config[$domain] = ['challenge' => 'dns-01']; + } + if (empty($domain_config)) throw new Exception('域名列表不能为空'); + + $order = $this->ac->createOrder($domain_config); + + $dnsList = []; + if (!empty($order['challenges'])) { + foreach ($order['challenges'] as $opts) { + $mainDomain = getMainDomain($opts['domain']); + $name = str_replace('.' . $mainDomain, '', $opts['key']); + /*if (!array_key_exists($mainDomain, $dnsList)) { + $dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "pki.goog"']; + }*/ + $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; + } + } + + return $dnsList; + } + + public function authOrder($domainList, $order) + { + $this->ac->authOrder($order); + } + + public function getAuthStatus($domainList, $order) + { + return true; + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + + if ($keytype == 'ECC') { + if (empty($keysize)) $keysize = '384'; + $private_key = $this->ac->generateECKey($keysize); + } else { + if (empty($keysize)) $keysize = '2048'; + $private_key = $this->ac->generateRSAKey($keysize); + } + $fullchain = $this->ac->finalizeOrder($domainList, $order, $private_key); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $certInfo['issuer']['CN'], 'subject' => $certInfo['subject']['CN'], 'validFrom' => $certInfo['validFrom_time_t'], 'validTo' => $certInfo['validTo_time_t']]; + } + + public function revoke($order, $pem) + { + $this->ac->revoke($pem); + } + + public function cancel($order) + { + } + + public function setLogger($func) + { + $this->ac->setLogger($func); + } +} diff --git a/app/lib/cert/huoshan.php b/app/lib/cert/huoshan.php new file mode 100644 index 0000000..b391a10 --- /dev/null +++ b/app/lib/cert/huoshan.php @@ -0,0 +1,163 @@ +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + $this->client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $this->service, $this->version, $this->region); + } + + public function register() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $this->request('GET', 'CertificateGetInstance', ['limit'=>1,'offset'=>0]); + return true; + } + + public function buyCert($domainList, &$order) + { + $data = $this->request('GET', 'CertificateGetOrganization'); + if(empty($data['content'])) throw new Exception('请先添加信息模板'); + $order['organization_id'] = $data['content'][0]['id']; + } + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + $domain = $domainList[0]; + $param = [ + 'plan' => 'digicert_free_standard_dv', + 'common_name' => $domain, + 'organization_id' => $order['organization_id'], + 'key_alg' => strtolower($keytype), + 'validation_type' => 'dns_txt', + ]; + $instance_id = $this->request('POST', 'QuickApplyCertificate', $param); + if(empty($instance_id)) throw new Exception('证书申请失败,证书实例ID为空'); + $order['instance_id'] = $instance_id; + + sleep(3); + + $param = [ + 'instance_id' => $instance_id, + ]; + $data = $this->request('GET', 'CertificateGetDcvParam', $param); + + $dnsList = []; + if (!empty($data['domains_to_be_validated'])) { + $type = $data['validation_type'] == 'dns_cname' ? 'CNAME' : 'TXT'; + foreach ($data['domains_to_be_validated'] as $opts) { + $mainDomain = getMainDomain($domain); + $name = str_replace('.' . $mainDomain, '', $opts['validation_domain']); + $dnsList[$mainDomain][] = ['name' => $name, 'type' => $type, 'value' => $opts['value']]; + } + } + return $dnsList; + } + + public function authOrder($domainList, $order) + { + $query = [ + 'instance_id' => $order['instance_id'], + ]; + $param = [ + 'action' => '', + ]; + $this->request('POST', 'CertificateProgressInstanceOrder', $param, $query); + } + + public function getAuthStatus($domainList, $order) + { + $param = [ + 'instance_id' => $order['instance_id'], + ]; + $data = $this->request('GET', 'CertificateGetInstance', $param); + if(empty($data['content'])) throw new Exception('证书信息获取失败'); + $data = $data['content'][0]; + if($data['order_status'] == 300 && $data['certificate_exist'] == 1){ + return true; + }elseif($data['order_status'] == 302){ + throw new Exception('证书申请失败'); + }else{ + return false; + } + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + $param = [ + 'instance_id' => $order['instance_id'], + ]; + $data = $this->request('GET', 'CertificateGetInstance', $param); + if (empty($data['content'])) throw new Exception('证书信息获取失败'); + $data = $data['content'][0]; + if (!isset($data['ssl']['certificate']['chain'])) throw new Exception('证书内容获取失败'); + + $fullchain = implode('', $data['ssl']['certificate']['chain']); + $private_key = $data['ssl']['certificate']['private_key']; + + return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $data['issuer'], 'subject' => $data['common_name']['CN'], 'validFrom' => intval($data['certificate_not_before_ms']/1000), 'validTo' => intval($data['certificate_not_after_ms']/1000)]; + } + + public function revoke($order, $pem) + { + $query = [ + 'instance_id' => $order['instance_id'], + ]; + $param = [ + 'action' => 'revoke', + 'reason' => '关联域名错误', + ]; + $this->request('POST', 'CertificateProgressInstanceOrder', $param, $query); + } + + public function cancel($order) + { + $query = [ + 'instance_id' => $order['instance_id'], + ]; + $param = [ + 'action' => 'cancel', + ]; + $this->request('POST', 'CertificateProgressInstanceOrder', $param, $query); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($method, $action, $params = [], $query = []) + { + $this->log('Action:'.$action.PHP_EOL.'Request:'.json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + $result = $this->client->request($method, $action, $params, $query); + if (is_array($result)) { + $this->log('Response:'.json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + } + return $result; + } +} diff --git a/app/lib/cert/letsencrypt.php b/app/lib/cert/letsencrypt.php new file mode 100644 index 0000000..66ba5cd --- /dev/null +++ b/app/lib/cert/letsencrypt.php @@ -0,0 +1,113 @@ + 'https://acme-v02.api.letsencrypt.org/directory', + 'staging' => 'https://acme-staging-v02.api.letsencrypt.org/directory' + ); + private $ac; + private $config; + private $ext; + + public function __construct($config, $ext = null) + { + $this->config = $config; + if (empty($config['mode'])) $config['mode'] = 'live'; + $this->ac = new ACMECert($this->directories[$config['mode']], $config['proxy'] == 1); + if ($ext) { + $this->ext = $ext; + $this->ac->loadAccountKey($ext['key']); + $this->ac->setAccount($ext['kid']); + } + } + + public function register() + { + if (empty($this->config['email'])) throw new Exception('邮件地址不能为空'); + + if (!empty($this->ext['key'])) { + $kid = $this->ac->register(true, $this->config['email']); + return ['kid' => $kid, 'key' => $this->ext['key']]; + } + + $key = $this->ac->generateRSAKey(2048); + $this->ac->loadAccountKey($key); + $kid = $this->ac->register(true, $this->config['email']); + return ['kid' => $kid, 'key' => $key]; + } + + public function buyCert($domainList, &$order) {} + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + $domain_config = []; + foreach ($domainList as $domain) { + if (empty($domain)) continue; + $domain_config[$domain] = ['challenge' => 'dns-01']; + } + if (empty($domain_config)) throw new Exception('域名列表不能为空'); + + $order = $this->ac->createOrder($domain_config); + + $dnsList = []; + if (!empty($order['challenges'])) { + foreach ($order['challenges'] as $opts) { + $mainDomain = getMainDomain($opts['domain']); + $name = str_replace('.' . $mainDomain, '', $opts['key']); + /*if (!array_key_exists($mainDomain, $dnsList)) { + $dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "letsencrypt.org"']; + }*/ + $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; + } + } + + return $dnsList; + } + + public function authOrder($domainList, $order) + { + $this->ac->authOrder($order); + } + + public function getAuthStatus($domainList, $order) + { + return true; + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + + if ($keytype == 'ECC') { + if (empty($keysize)) $keysize = '384'; + $private_key = $this->ac->generateECKey($keysize); + } else { + if (empty($keysize)) $keysize = '2048'; + $private_key = $this->ac->generateRSAKey($keysize); + } + $fullchain = $this->ac->finalizeOrder($domainList, $order, $private_key); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $certInfo['issuer']['CN'], 'subject' => $certInfo['subject']['CN'], 'validFrom' => $certInfo['validFrom_time_t'], 'validTo' => $certInfo['validTo_time_t']]; + } + + public function revoke($order, $pem) + { + $this->ac->revoke($pem); + } + + public function cancel($order) {} + + public function setLogger($func) + { + $this->ac->setLogger($func); + } +} diff --git a/app/lib/cert/tencent.php b/app/lib/cert/tencent.php new file mode 100644 index 0000000..fc339d9 --- /dev/null +++ b/app/lib/cert/tencent.php @@ -0,0 +1,189 @@ +SecretId = $config['SecretId']; + $this->SecretKey = $config['SecretKey']; + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, $this->endpoint, $this->service, $this->version); + $this->email = $config['email']; + } + + public function register() + { + if (empty($this->SecretId) || empty($this->SecretKey) || empty($this->email)) throw new Exception('必填参数不能为空'); + $this->request('DescribeCertificates', []); + return true; + } + + public function buyCert($domainList, &$order) {} + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + $domain = $domainList[0]; + $param = [ + 'DvAuthMethod' => 'DNS', + 'DomainName' => $domain, + 'ContactEmail' => $this->email, + 'CsrEncryptAlgo' => $keytype, + 'CsrKeyParameter' => $keytype == 'ECC' ? 'prime256v1' : '2048', + ]; + $data = $this->request('ApplyCertificate', $param); + if (empty($data['CertificateId'])) throw new Exception('证书申请失败,CertificateId为空'); + $order['CertificateId'] = $data['CertificateId']; + + $param = [ + 'CertificateId' => $order['CertificateId'], + ]; + $data = $this->request('DescribeCertificate', $param); + $order['OrderId'] = $data['OrderId']; + + $dnsList = []; + if (!empty($data['DvAuthDetail']['DvAuths'])) { + foreach ($data['DvAuthDetail']['DvAuths'] as $opts) { + $mainDomain = $opts['DvAuthDomain']; + $dnsList[$mainDomain][] = ['name' => $opts['DvAuthSubDomain'], 'type' => $opts['DvAuthVerifyType'] ?? 'CNAME', 'value' => $opts['DvAuthValue']]; + } + } + + return $dnsList; + } + + public function authOrder($domainList, $order) + { + $param = [ + 'CertificateId' => $order['CertificateId'], + ]; + $data = $this->request('DescribeCertificate', $param); + if ($data['Status'] == 0 || $data['Status'] == 4) { + $this->request('CompleteCertificate', $param); + sleep(3); + } + } + + public function getAuthStatus($domainList, $order) + { + $param = [ + 'CertificateId' => $order['CertificateId'], + ]; + $data = $this->request('DescribeCertificate', $param); + if ($data['Status'] == 1) { + return true; + } elseif ($data['Status'] == 2) { + throw new Exception('证书审核失败' . (empty($data['StatusMsg'] ? '' : ':' . $data['StatusMsg']))); + } else { + return false; + } + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + if (!is_dir(app()->getRuntimePath() . 'cert')) mkdir(app()->getRuntimePath() . 'cert'); + $param = [ + 'CertificateId' => $order['CertificateId'], + 'ServiceType' => 'nginx', + ]; + $data = $this->request('DescribeDownloadCertificateUrl', $param); + $file_data = get_curl($data['DownloadCertificateUrl']); + $file_path = app()->getRuntimePath() . 'cert/' . $data['DownloadFilename']; + $file_name = substr($data['DownloadFilename'], 0, -4); + file_put_contents($file_path, $file_data); + + $zip = new \ZipArchive; + if ($zip->open($file_path) === true) { + $zip->extractTo(app()->getRuntimePath() . 'cert/'); + $zip->close(); + } else { + throw new Exception('解压证书失败'); + } + $cert_dir = app()->getRuntimePath() . 'cert/' . $file_name; + + $items = scandir($cert_dir); + if ($items === false) throw new Exception('解压后的证书文件夹不存在'); + $private_key = null; + $fullchain = null; + foreach ($items as $item) { + if (substr($item, -4) == '.key') { + $private_key = file_get_contents($cert_dir . '/' . $item); + } elseif (substr($item, -4) == '.crt') { + $fullchain = file_get_contents($cert_dir . '/' . $item); + } + } + if (empty($private_key) || empty($fullchain)) throw new Exception('解压后的证书文件夹内未找到证书文件'); + + clearDirectory($cert_dir); + rmdir($cert_dir); + unlink($file_path); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $certInfo['issuer']['CN'], 'subject' => $certInfo['subject']['CN'], 'validFrom' => $certInfo['validFrom_time_t'], 'validTo' => $certInfo['validTo_time_t']]; + } + + public function revoke($order, $pem) + { + $param = [ + 'CertificateId' => $order['CertificateId'], + ]; + $action = 'RevokeCertificate'; + $data = $this->request($action, $param); + + if (!empty($data['RevokeDomainValidateAuths'])) { + $dnsList = []; + foreach ($data['RevokeDomainValidateAuths'] as $opts) { + $mainDomain = getMainDomain($opts['DomainValidateAuthDomain']); + $name = str_replace('.' . $mainDomain, '', $opts['DomainValidateAuthKey']); + $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'CNAME', 'value' => $opts['DomainValidateAuthValue']]; + } + \app\utils\CertDnsUtils::addDns($dnsList, function ($txt) { + $this->log($txt); + }); + } + } + + public function cancel($order) + { + $param = [ + 'CertificateId' => $order['CertificateId'], + ]; + $action = 'CancelAuditCertificate'; + $this->request($action, $param); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($action, $param) + { + $this->log('Action:' . $action . PHP_EOL . 'Request:' . json_encode($param, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + $result = $this->client->request($action, $param); + $this->log('Response:' . json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return $result; + } +} diff --git a/app/lib/cert/ucloud.php b/app/lib/cert/ucloud.php new file mode 100644 index 0000000..151da3e --- /dev/null +++ b/app/lib/cert/ucloud.php @@ -0,0 +1,187 @@ +PublicKey = $config['PublicKey']; + $this->PrivateKey = $config['PrivateKey']; + $this->client = new UcloudClient($this->PublicKey, $this->PrivateKey); + $this->config = $config; + } + + public function register() + { + if (empty($this->PublicKey) || empty($this->PrivateKey) || empty($this->config['username']) || empty($this->config['phone']) || empty($this->config['email'])) throw new Exception('必填参数不能为空'); + $param = ['Mode' => 'free']; + $this->request('GetCertificateList', $param); + return true; + } + + public function buyCert($domainList, &$order) + { + $param = [ + 'CertificateBrand' => 'TrustAsia', + 'CertificateName' => 'TrustAsiaC1DVFree', + 'DomainsCount' => 1, + 'ValidYear' => 1, + ]; + $data = $this->request('PurchaseCertificate', $param); + if (!isset($data['CertificateID'])) throw new Exception('证书购买失败,CertificateID为空'); + $order['CertificateID'] = $data['CertificateID']; + } + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + $domain = $domainList[0]; + $param = [ + 'CertificateID' => $order['CertificateID'], + 'Domains' => $domain, + 'CSROnline' => 1, + 'CSREncryptAlgo' => ['RSA' => 'RSA', 'ECC' => 'ECDSA'][$keytype], + 'CSRKeyParameter' => ['2048' => '2048', '3072' => '3072', '256' => 'prime256v1', '384' => 'prime384v1'][$keysize], + 'CompanyName' => '公司名称', + 'CompanyAddress' => '公司地址', + 'CompanyRegion' => '北京', + 'CompanyCity' => '北京', + 'CompanyCountry' => 'CN', + 'CompanyDivision' => '部门', + 'CompanyPhone' => $this->config['phone'], + 'CompanyPostalCode' => '110100', + 'AdminName' => $this->config['username'], + 'AdminPhone' => $this->config['phone'], + 'AdminEmail' => $this->config['email'], + 'AdminTitle' => '职员', + 'DVAuthMethod' => 'DNS' + ]; + $data = $this->request('ComplementCSRInfo', $param); + + sleep(3); + + $param = [ + 'CertificateID' => $order['CertificateID'], + ]; + $data = $this->request('GetDVAuthInfo', $param); + + $dnsList = []; + if (!empty($data['Auths'])) { + foreach ($data['Auths'] as $auth) { + $mainDomain = getMainDomain($auth['Domain']); + $dnsList[$mainDomain][] = ['name' => $auth['AuthRecord'], 'type' => $auth['AuthType'] == 'DNS_TXT' ? 'TXT' : 'CNAME', 'value' => $auth['AuthValue']]; + } + } + return $dnsList; + } + + public function authOrder($domainList, $order) {} + + public function getAuthStatus($domainList, $order) + { + $param = [ + 'CertificateID' => $order['CertificateID'], + ]; + $data = $this->request('GetCertificateDetailInfo', $param); + if ($data['CertificateInfo']['StateCode'] == 'COMPLETED' || $data['CertificateInfo']['StateCode'] == 'RENEWED') { + return true; + } elseif ($data['CertificateInfo']['StateCode'] == 'REJECTED' || $data['CertificateInfo']['StateCode'] == 'SECURITY_REVIEW_FAILED') { + throw new Exception('证书审核失败:' . $data['CertificateInfo']['State']); + } else { + return false; + } + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + if (!is_dir(app()->getRuntimePath() . 'cert')) mkdir(app()->getRuntimePath() . 'cert'); + $param = [ + 'CertificateID' => $order['CertificateID'], + ]; + $info = $this->request('GetCertificateDetailInfo', $param); + + $data = $this->request('DownloadCertificate', $param); + $file_data = get_curl($data['CertificateUrl']); + $file_path = app()->getRuntimePath() . 'cert/USSL_' . $order['CertificateID'] . '.zip'; + file_put_contents($file_path, $file_data); + + $zip = new \ZipArchive; + if ($zip->open($file_path) === true) { + $zip->extractTo(app()->getRuntimePath() . 'cert/'); + $zip->close(); + } else { + throw new Exception('解压证书失败'); + } + $cert_dir = app()->getRuntimePath() . 'cert/Nginx'; + + $items = scandir($cert_dir); + if ($items === false) throw new Exception('解压后的证书文件夹不存在'); + $private_key = null; + $fullchain = null; + foreach ($items as $item) { + if (substr($item, -4) == '.key') { + $private_key = file_get_contents($cert_dir . '/' . $item); + } elseif (substr($item, -4) == '.pem') { + $fullchain = file_get_contents($cert_dir . '/' . $item); + } + } + if (empty($private_key) || empty($fullchain)) throw new Exception('解压后的证书文件夹内未找到证书文件'); + + clearDirectory(app()->getRuntimePath() . 'cert'); + + return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $info['CertificateInfo']['CaOrganization'], 'subject' => $info['CertificateInfo']['Name'], 'validFrom' => $info['CertificateInfo']['IssuedDate'], 'validTo' => $info['CertificateInfo']['ExpiredDate']]; + } + + public function revoke($order, $pem) + { + $param = [ + 'CertificateID' => $order['CertificateID'], + 'Reason' => '业务终止', + ]; + $this->request('RevokeCertificate', $param); + } + + public function cancel($order) + { + $param = [ + 'CertificateID' => $order['CertificateID'], + ]; + $this->request('CancelCertificateOrder', $param); + + sleep(1); + + $param['CertificateMode'] = 'purchase'; + $this->request('DeleteSSLCertificate', $param); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($action, $params) + { + $this->log('Action:' . $action . PHP_EOL . 'Request:' . json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + $result = $this->client->request($action, $params); + $this->log('Response:' . json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return $result; + } +} diff --git a/app/lib/cert/zerossl.php b/app/lib/cert/zerossl.php new file mode 100644 index 0000000..9b1ff17 --- /dev/null +++ b/app/lib/cert/zerossl.php @@ -0,0 +1,110 @@ +config = $config; + $this->ac = new ACMECert($this->directory, $config['proxy'] == 1); + if ($ext) { + $this->ext = $ext; + $this->ac->loadAccountKey($ext['key']); + $this->ac->setAccount($ext['kid']); + } + } + + public function register() + { + if (empty($this->config['email'])) throw new Exception('邮件地址不能为空'); + if (empty($this->config['kid']) || empty($this->config['key'])) throw new Exception('必填参数不能为空'); + + if (!empty($this->ext['key'])) { + $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); + return ['kid' => $kid, 'key' => $this->ext['key']]; + } + + $key = $this->ac->generateRSAKey(2048); + $this->ac->loadAccountKey($key); + $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); + return ['kid' => $kid, 'key' => $key]; + } + + public function buyCert($domainList, &$order) {} + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + $domain_config = []; + foreach ($domainList as $domain) { + if (empty($domain)) continue; + $domain_config[$domain] = ['challenge' => 'dns-01']; + } + if (empty($domain_config)) throw new Exception('域名列表不能为空'); + + $order = $this->ac->createOrder($domain_config); + + $dnsList = []; + if (!empty($order['challenges'])) { + foreach ($order['challenges'] as $opts) { + $mainDomain = getMainDomain($opts['domain']); + $name = str_replace('.' . $mainDomain, '', $opts['key']); + /*if (!array_key_exists($mainDomain, $dnsList)) { + $dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "sectigo.com"']; + }*/ + $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; + } + } + + return $dnsList; + } + + public function authOrder($domainList, $order) + { + $this->ac->authOrder($order); + } + + public function getAuthStatus($domainList, $order) + { + return true; + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + + if ($keytype == 'ECC') { + if (empty($keysize)) $keysize = '384'; + $private_key = $this->ac->generateECKey($keysize); + } else { + if (empty($keysize)) $keysize = '2048'; + $private_key = $this->ac->generateRSAKey($keysize); + } + $fullchain = $this->ac->finalizeOrder($domainList, $order, $private_key); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $certInfo['issuer']['CN'], 'subject' => $certInfo['subject']['CN'], 'validFrom' => $certInfo['validFrom_time_t'], 'validTo' => $certInfo['validTo_time_t']]; + } + + public function revoke($order, $pem) + { + $this->ac->revoke($pem); + } + + public function cancel($order) {} + + public function setLogger($func) + { + $this->ac->setLogger($func); + } +} diff --git a/app/lib/client/AWS.php b/app/lib/client/AWS.php new file mode 100644 index 0000000..4c7d5cf --- /dev/null +++ b/app/lib/client/AWS.php @@ -0,0 +1,330 @@ +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->endpoint = $endpoint; + $this->service = $service; + $this->version = $version; + $this->region = $region; + } + + /** + * @param string $method 请求方法 + * @param string $action 方法名称 + * @param array $params 请求参数 + * @return array + * @throws Exception + */ + public function request($method, $action, $params = []) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + $body = ''; + $query = []; + if ($method == 'GET' || $method == 'DELETE') { + $query = $params; + } else { + $body = !empty($params) ? json_encode($params) : ''; + } + + $time = time(); + $date = gmdate("Ymd\THis\Z", $time); + $headers = [ + 'Host' => $this->endpoint, + 'X-Amz-Target' => $action, + 'X-Amz-Date' => $date, + //'X-Amz-Content-Sha256' => hash("sha256", $body), + ]; + if ($body) { + $headers['Content-Type'] = 'application/x-amz-json-1.1'; + } + $path = '/'; + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $date); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + /** + * @param string $method 请求方法 + * @param string $action 方法名称 + * @param array $params 请求参数 + * @return array + * @throws Exception + */ + public function requestXml($method, $action, $params = []) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + $body = ''; + $query = [ + 'Action' => $action, + 'Version' => $this->version, + ]; + if ($method == 'GET' || $method == 'DELETE') { + $query = array_merge($query, $params); + } else { + $body = !empty($params) ? http_build_query($params) : ''; + } + + $time = time(); + $date = gmdate("Ymd\THis\Z", $time); + $headers = [ + 'Host' => $this->endpoint, + 'X-Amz-Date' => $date, + ]; + + $path = '/'; + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $date); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header, true); + } + + /** + * @param string $method 请求方法 + * @param string $path 请求路径 + * @param array $params 请求参数 + * @param \SimpleXMLElement $xml 请求XML + * @return array + * @throws Exception + */ + public function requestXmlN($method, $path, $params = [], $xml = null, $etag = false) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + $path = '/' . $this->version . $path; + if ($method == 'GET' || $method == 'DELETE') { + $query = $params; + } else { + $body = !empty($params) ? $this->array2xml($params, $xml) : ''; + } + + $time = time(); + $date = gmdate("Ymd\THis\Z", $time); + $headers = [ + 'Host' => $this->endpoint, + 'X-Amz-Date' => $date, + //'X-Amz-Content-Sha256' => hash("sha256", $body), + ]; + if ($this->etag) { + $headers['If-Match'] = $this->etag; + } + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $date); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header, true, $etag); + } + + private function generateSign($method, $path, $query, $headers, $body, $date) + { + $algorithm = "AWS4-HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $path; + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + $canonicalRequest = $httpRequestMethod . "\n" + . $canonicalUri . "\n" + . $canonicalQueryString . "\n" + . $canonicalHeaders . "\n" + . $signedHeaders . "\n" + . $hashedRequestPayload; + + // step 2: build string to sign + $shortDate = substr($date, 0, 8); + $credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/aws4_request'; + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = $algorithm . "\n" + . $date . "\n" + . $credentialScope . "\n" + . $hashedCanonicalRequest; + + // step 3: sign string + $kDate = hash_hmac("sha256", $shortDate, 'AWS4' . $this->SecretAccessKey, true); + $kRegion = hash_hmac("sha256", $this->region, $kDate, true); + $kService = hash_hmac("sha256", $this->service, $kRegion, true); + $kSigning = hash_hmac("sha256", "aws4_request", $kService, true); + $signature = hash_hmac("sha256", $stringToSign, $kSigning); + + // step 4: build authorization + $credential = $this->AccessKeyId . '/' . $credentialScope; + $authorization = $algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header, $xml = false, $etag = false) + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + if ($etag) { + curl_setopt($ch, CURLOPT_HEADER, true); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + curl_close($ch); + throw new Exception('Curl error: ' . curl_error($ch)); + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($etag) { + if (preg_match('/ETag: ([^\r\n]+)/', $response, $matches)) { + $this->etag = trim($matches[1]); + } + $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $response = substr($response, $headerSize); + } + curl_close($ch); + + if ($httpCode >= 200 && $httpCode < 300) { + if (empty($response)) return true; + return $xml ? $this->xml2array($response) : json_decode($response, true); + } + if ($xml) { + $arr = $this->xml2array($response); + if (isset($arr['Error']['Message'])) { + throw new Exception($arr['Error']['Message']); + } else { + throw new Exception('HTTP Code: ' . $httpCode); + } + } else { + $arr = json_decode($response, true); + if (isset($arr['message'])) { + throw new Exception($arr['message']); + } else { + throw new Exception('HTTP Code: ' . $httpCode); + } + } + } + + private function xml2array($xml) + { + if (!$xml) { + return false; + } + LIBXML_VERSION < 20900 && libxml_disable_entity_loader(true); + return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true); + } + + private function array2xml($array, $xml = null) + { + if ($xml === null) { + $xml = new \SimpleXMLElement(''); + } + + foreach ($array as $key => $value) { + if (is_array($value)) { + $subNode = $xml->addChild($key); + $this->array2xml($value, $subNode); + } else { + $xml->addChild($key, $value); + } + } + + return $xml->asXML(); + } +} diff --git a/app/lib/client/Aliyun.php b/app/lib/client/Aliyun.php new file mode 100644 index 0000000..f617e23 --- /dev/null +++ b/app/lib/client/Aliyun.php @@ -0,0 +1,95 @@ +AccessKeyId = $AccessKeyId; + $this->AccessKeySecret = $AccessKeySecret; + $this->Endpoint = $Endpoint; + $this->Version = $Version; + } + + /** + * @param array $param 请求参数 + * @return bool|array + * @throws Exception + */ + public function request($param, $method = 'POST') + { + $url = 'https://' . $this->Endpoint . '/'; + $data = array( + 'Format' => 'JSON', + 'Version' => $this->Version, + 'AccessKeyId' => $this->AccessKeyId, + 'SignatureMethod' => 'HMAC-SHA1', + 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), + 'SignatureVersion' => '1.0', + 'SignatureNonce' => random(8) + ); + $data = array_merge($data, $param); + $data['Signature'] = $this->aliyunSignature($data, $this->AccessKeySecret, $method); + if ($method == 'GET') { + $url .= '?' . http_build_query($data); + } + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + if ($method == 'POST') { + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($errno) { + curl_close($ch); + throw new Exception('Curl error: ' . curl_error($ch)); + } + curl_close($ch); + + $arr = json_decode($response, true); + if ($httpCode == 200) { + return $arr; + } elseif ($arr) { + throw new Exception($arr['Message']); + } else { + throw new Exception('返回数据解析失败'); + } + } + + private function aliyunSignature($parameters, $accessKeySecret, $method) + { + ksort($parameters); + $canonicalizedQueryString = ''; + foreach ($parameters as $key => $value) { + if ($value === null) continue; + $canonicalizedQueryString .= '&' . $this->percentEncode($key) . '=' . $this->percentEncode($value); + } + $stringToSign = $method . '&%2F&' . $this->percentEncode(substr($canonicalizedQueryString, 1)); + $signature = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&", true)); + + return $signature; + } + + private function percentEncode($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } +} diff --git a/app/lib/client/AliyunNew.php b/app/lib/client/AliyunNew.php new file mode 100644 index 0000000..d37f358 --- /dev/null +++ b/app/lib/client/AliyunNew.php @@ -0,0 +1,169 @@ +AccessKeyId = $AccessKeyId; + $this->AccessKeySecret = $AccessKeySecret; + $this->Endpoint = $Endpoint; + $this->Version = $Version; + } + + /** + * @param string $method 请求方法 + * @param string $action 操作名称 + * @param array|null $params 请求参数 + * @return array + * @throws Exception + */ + public function request($method, $action, $path = '/', $params = null) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { return $a !== null;}); + } + + if($method == 'GET' || $method == 'DELETE'){ + $query = $params; + $body = ''; + }else{ + $query = []; + $body = !empty($params) ? json_encode($params) : ''; + } + $headers = [ + 'x-acs-action' => $action, + 'x-acs-version' => $this->Version, + 'x-acs-signature-nonce' => md5(uniqid(mt_rand(), true) . microtime()), + 'x-acs-date' => gmdate('Y-m-d\TH:i:s\Z'), + 'x-acs-content-sha256' => hash("sha256", $body), + 'Host' => $this->Endpoint, + ]; + if ($body) { + $headers['Content-Type'] = 'application/json; charset=utf-8'; + } + + $authorization = $this->generateSign($method, $path, $query, $headers, $body); + $headers['Authorization'] = $authorization; + + $url = 'https://'.$this->Endpoint.$path; + if (!empty($query)) { + $url .= '?'.http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key.': '.$value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($method, $path, $query, $headers, $body) + { + $algorithm = "ACS3-HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $path; + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + $canonicalRequest = $httpRequestMethod."\n" + .$canonicalUri."\n" + .$canonicalQueryString."\n" + .$canonicalHeaders."\n" + .$signedHeaders."\n" + .$hashedRequestPayload; + + // step 2: build string to sign + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = $algorithm."\n" + .$hashedCanonicalRequest; + + // step 3: sign string + $signature = hash_hmac("sha256", $stringToSign, $this->AccessKeySecret); + + // step 4: build authorization + $authorization = $algorithm . ' Credential=' . $this->AccessKeyId . ',SignedHeaders=' . $signedHeaders . ',Signature=' . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + $canonicalQueryString .= '&' . $this->escape($key). '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + curl_close($ch); + throw new Exception('Curl error: ' . curl_error($ch)); + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $arr = json_decode($response, true); + if ($httpCode == 200) { + return $arr; + } elseif ($arr) { + if(strpos($arr['Message'], '.') > 0) $arr['Message'] = substr($arr['Message'], 0, strpos($arr['Message'], '.')+1); + throw new Exception($arr['Message']); + } else { + throw new Exception('返回数据解析失败'); + } + } +} \ No newline at end of file diff --git a/app/lib/client/AliyunOss.php b/app/lib/client/AliyunOss.php new file mode 100644 index 0000000..ab2f5b8 --- /dev/null +++ b/app/lib/client/AliyunOss.php @@ -0,0 +1,261 @@ +AccessKeyId = $AccessKeyId; + $this->AccessKeySecret = $AccessKeySecret; + $this->Endpoint = $Endpoint; + } + + public function addBucketCnameCert($bucket, $domain, $cert_id) + { + $strXml = << + + + EOF; + $xml = new \SimpleXMLElement($strXml); + $node = $xml->addChild('Cname'); + $node->addChild('Domain', $domain); + $certNode = $node->addChild('CertificateConfiguration'); + $certNode->addChild('CertId', $cert_id); + $certNode->addChild('Force', 'true'); + $body = $xml->asXML(); + + $options = [ + 'bucket' => $bucket, + 'key' => '', + ]; + $query = [ + 'cname' => '', + 'comp' => 'add' + ]; + return $this->request('POST', '/', $query, $body, $options); + } + + public function deleteBucketCnameCert($bucket, $domain) + { + $strXml = << + + + EOF; + $xml = new \SimpleXMLElement($strXml); + $node = $xml->addChild('Cname'); + $node->addChild('Domain', $domain); + $certNode = $node->addChild('CertificateConfiguration'); + $certNode->addChild('DeleteCertificate', 'true'); + $body = $xml->asXML(); + + $options = [ + 'bucket' => $bucket, + 'key' => '', + ]; + $query = [ + 'cname' => '', + 'comp' => 'add' + ]; + return $this->request('POST', '/', $query, $body, $options); + } + + public function getBucketCname($bucket) + { + $options = [ + 'bucket' => $bucket, + 'key' => '', + ]; + $query = [ + 'cname' => '', + ]; + return $this->request('GET', '/', $query, null, $options); + } + + private function request($method, $path, $query, $body, $options) + { + $hostname = $options['bucket'] . '.' . $this->Endpoint; + $query_string = $this->toQueryString($query); + $query_string = empty($query_string) ? '' : '?' . $query_string; + $requestUrl = 'https://' . $hostname . $path . $query_string; + $headers = [ + 'Content-Type' => 'application/xml', + 'Date' => gmdate('D, d M Y H:i:s \G\M\T'), + ]; + $headers['Authorization'] = $this->getAuthorization($method, $path, $query, $headers, $options); + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $requestUrl, $body, $header); + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($errno) { + curl_close($ch); + throw new Exception('Curl error: ' . curl_error($ch)); + } + curl_close($ch); + + if ($httpCode >= 200 && $httpCode < 300) { + if (empty($response)) return true; + return $this->xml2array($response); + } + $arr = $this->xml2array($response); + if (isset($arr['Message'])) { + throw new Exception($arr['Message']); + } else { + throw new Exception('HTTP Code: ' . $httpCode); + } + } + + private function toQueryString($params = array()) + { + $temp = array(); + uksort($params, 'strnatcasecmp'); + foreach ($params as $key => $value) { + if (is_string($key) && !is_array($value)) { + if (strlen($value) > 0) { + $temp[] = rawurlencode($key) . '=' . rawurlencode($value); + } else { + $temp[] = rawurlencode($key); + } + } + } + return implode('&', $temp); + } + + private function xml2array($xml) + { + if (!$xml) { + return false; + } + LIBXML_VERSION < 20900 && libxml_disable_entity_loader(true); + return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true); + } + + private function getAuthorization($method, $url, $query, $headers, $options) + { + $method = strtoupper($method); + $date = $headers['Date']; + $resourcePath = $this->getResourcePath($options); + $stringToSign = $this->calcStringToSign($method, $date, $headers, $resourcePath, $query); + $signature = base64_encode(hash_hmac('sha1', $stringToSign, $this->AccessKeySecret, true)); + return 'OSS ' . $this->AccessKeyId . ':' . $signature; + } + + private function getResourcePath(array $options) + { + $resourcePath = '/'; + if (strlen($options['bucket']) > 0) { + $resourcePath .= $options['bucket'] . '/'; + } + if (strlen($options['key']) > 0) { + $resourcePath .= $options['key']; + } + return $resourcePath; + } + + private function calcStringToSign($method, $date, array $headers, $resourcePath, array $query) + { + /* + SignToString = + VERB + "\n" + + Content-MD5 + "\n" + + Content-Type + "\n" + + Date + "\n" + + CanonicalizedOSSHeaders + + CanonicalizedResource + Signature = base64(hmac-sha1(AccessKeySecret, SignToString)) + */ + $contentMd5 = ''; + $contentType = ''; + // CanonicalizedOSSHeaders + $signheaders = array(); + foreach ($headers as $key => $value) { + $lowk = strtolower($key); + if (strncmp($lowk, "x-oss-", 6) == 0) { + $signheaders[$lowk] = $value; + } else if ($lowk === 'content-md5') { + $contentMd5 = $value; + } else if ($lowk === 'content-type') { + $contentType = $value; + } + } + ksort($signheaders); + $canonicalizedOSSHeaders = ''; + foreach ($signheaders as $key => $value) { + $canonicalizedOSSHeaders .= $key . ':' . $value . "\n"; + } + // CanonicalizedResource + $signquery = array(); + foreach ($query as $key => $value) { + if (in_array($key, $this->signKeyList)) { + $signquery[$key] = $value; + } + } + ksort($signquery); + $sortedQueryList = array(); + foreach ($signquery as $key => $value) { + if (strlen($value) > 0) { + $sortedQueryList[] = $key . '=' . $value; + } else { + $sortedQueryList[] = $key; + } + } + $queryStringSorted = implode('&', $sortedQueryList); + $canonicalizedResource = $resourcePath; + if (!empty($queryStringSorted)) { + $canonicalizedResource .= '?' . $queryStringSorted; + } + return $method . "\n" . $contentMd5 . "\n" . $contentType . "\n" . $date . "\n" . $canonicalizedOSSHeaders . $canonicalizedResource; + } + + private $signKeyList = array( + "acl", "uploads", "location", "cors", + "logging", "website", "referer", "lifecycle", + "delete", "append", "tagging", "objectMeta", + "uploadId", "partNumber", "security-token", "x-oss-security-token", + "position", "img", "style", "styleName", + "replication", "replicationProgress", + "replicationLocation", "cname", "bucketInfo", + "comp", "qos", "live", "status", "vod", + "startTime", "endTime", "symlink", + "x-oss-process", "response-content-type", "x-oss-traffic-limit", + "response-content-language", "response-expires", + "response-cache-control", "response-content-disposition", + "response-content-encoding", "udf", "udfName", "udfImage", + "udfId", "udfImageDesc", "udfApplication", + "udfApplicationLog", "restore", "callback", "callback-var", "qosInfo", + "policy", "stat", "encryption", "versions", "versioning", "versionId", "requestPayment", + "x-oss-request-payer", "sequential", + "inventory", "inventoryId", "continuation-token", "asyncFetch", + "worm", "wormId", "wormExtend", "withHashContext", + "x-oss-enable-md5", "x-oss-enable-sha1", "x-oss-enable-sha256", + "x-oss-hash-ctx", "x-oss-md5-ctx", "transferAcceleration", + "regionList", "cloudboxes", "x-oss-ac-source-ip", "x-oss-ac-subnet-mask", "x-oss-ac-vpc-id", "x-oss-ac-forward-allow", + "metaQuery", "resourceGroup", "rtc", "x-oss-async-process", "responseHeader" + ); +} \ No newline at end of file diff --git a/app/lib/client/BaiduCloud.php b/app/lib/client/BaiduCloud.php new file mode 100644 index 0000000..551dcd8 --- /dev/null +++ b/app/lib/client/BaiduCloud.php @@ -0,0 +1,175 @@ +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->endpoint = $endpoint; + } + + /** + * @param string $method 请求方法 + * @param string $path 请求路径 + * @param array|null $query 请求参数 + * @param array|null $params 请求体 + * @return array + * @throws Exception + */ + public function request($method, $path, $query = null, $params = null) + { + if (!empty($query)) { + $query = array_filter($query, function ($a) { return $a !== null;}); + } + if (!empty($params)) { + $params = array_filter($params, function ($a) { return $a !== null;}); + } + + $time = time(); + $date = gmdate("Y-m-d\TH:i:s\Z", $time); + $body = !empty($params) ? json_encode($params) : ''; + $headers = [ + 'Host' => $this->endpoint, + 'x-bce-date' => $date, + ]; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + + $authorization = $this->generateSign($method, $path, $query, $headers, $time); + $headers['Authorization'] = $authorization; + + $url = 'https://'.$this->endpoint.$path; + if (!empty($query)) { + $url .= '?'.http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key.': '.$value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($method, $path, $query, $headers, $time) + { + $algorithm = "bce-auth-v1"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $this->getCanonicalUri($path); + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $canonicalRequest = $httpRequestMethod."\n" + .$canonicalUri."\n" + .$canonicalQueryString."\n" + .$canonicalHeaders; + + // step 2: calculate signing key + $date = gmdate("Y-m-d\TH:i:s\Z", $time); + $expirationInSeconds = 1800; + $authString = $algorithm . '/' . $this->AccessKeyId . '/' . $date . '/' . $expirationInSeconds; + $signingKey = hash_hmac('sha256', $authString, $this->SecretAccessKey); + + // step 3: sign string + $signature = hash_hmac("sha256", $canonicalRequest, $signingKey); + + // step 4: build authorization + $authorization = $authString . '/' . $signedHeaders . "/" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalUri($path) + { + if (empty($path)) return '/'; + $uri = str_replace('%2F', '/', $this->escape($path)); + if (substr($uri, 0, 1) !== '/') $uri = '/' . $uri; + return $uri; + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + if ($key == 'authorization') continue; + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $this->escape($key) . ':' . $this->escape($value) . "\n"; + $signedHeaders .= $key . ';'; + } + $canonicalHeaders = substr($canonicalHeaders, 0, -1); + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($errno) { + curl_close($ch); + throw new Exception('Curl error: ' . curl_error($ch)); + } + curl_close($ch); + + if (empty($response) && $httpCode == 200) { + return true; + } + $arr = json_decode($response, true); + if ($arr) { + if (isset($arr['code']) && isset($arr['message'])) { + throw new Exception($arr['message']); + } else { + return $arr; + } + } else { + throw new Exception('返回数据解析失败'); + } + } +} \ No newline at end of file diff --git a/app/lib/client/HuaweiCloud.php b/app/lib/client/HuaweiCloud.php new file mode 100644 index 0000000..e5942b3 --- /dev/null +++ b/app/lib/client/HuaweiCloud.php @@ -0,0 +1,175 @@ +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->endpoint = $endpoint; + } + + /** + * @param string $method 请求方法 + * @param string $path 请求路径 + * @param array|null $query 请求参数 + * @param array|null $params 请求体 + * @return array + * @throws Exception + */ + public function request($method, $path, $query = null, $params = null) + { + if (!empty($query)) { + $query = array_filter($query, function ($a) { return $a !== null;}); + } + if (!empty($params)) { + $params = array_filter($params, function ($a) { return $a !== null;}); + } + + $time = time(); + $date = gmdate("Ymd\THis\Z", $time); + $body = !empty($params) ? json_encode($params) : ''; + $headers = [ + 'Host' => $this->endpoint, + 'X-Sdk-Date' => $date, + ]; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($method, $path, $query, $headers, $body, $time) + { + $algorithm = "SDK-HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $path; + if (substr($canonicalUri, -1) != "/") $canonicalUri .= "/"; + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + $canonicalRequest = $httpRequestMethod . "\n" + . $canonicalUri . "\n" + . $canonicalQueryString . "\n" + . $canonicalHeaders . "\n" + . $signedHeaders . "\n" + . $hashedRequestPayload; + + // step 2: build string to sign + $date = gmdate("Ymd\THis\Z", $time); + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = $algorithm . "\n" + . $date . "\n" + . $hashedCanonicalRequest; + + // step 3: sign string + $signature = hash_hmac("sha256", $stringToSign, $this->SecretAccessKey); + + // step 4: build authorization + $authorization = $algorithm . ' Access=' . $this->AccessKeyId . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + curl_close($ch); + throw new Exception('Curl error: ' . curl_error($ch)); + } + curl_close($ch); + + $arr = json_decode($response, true); + if ($arr) { + if (isset($arr['error_msg'])) { + throw new Exception($arr['error_msg']); + } elseif (isset($arr['message'])) { + throw new Exception($arr['message']); + } elseif (isset($arr['error']['error_msg'])) { + throw new Exception($arr['error']['error_msg']); + } else { + return $arr; + } + } else { + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($httpCode >= 200 && $httpCode < 300) { + return null; + } else { + throw new Exception('返回数据解析失败'); + } + } + } +} diff --git a/app/lib/client/Qiniu.php b/app/lib/client/Qiniu.php new file mode 100644 index 0000000..20feabf --- /dev/null +++ b/app/lib/client/Qiniu.php @@ -0,0 +1,104 @@ +AccessKey = $AccessKey; + $this->SecretKey = $SecretKey; + } + + /** + * @param string $method 请求方法 + * @param string $path 请求路径 + * @param array|null $query 请求参数 + * @param array|null $params 请求体 + * @return array + * @throws Exception + */ + public function request($method, $path, $query = null, $params = null) + { + $url = $this->ApiUrl . $path; + $query_str = null; + $body = null; + if (!empty($query)) { + $query = array_filter($query, function ($a) { + return $a !== null; + }); + $query_str = http_build_query($query); + $url .= '?' . $query_str; + } + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + $body = json_encode($params); + } + + $sign_str = $path . ($query_str ? '?' . $query_str : '') . "\n"; + $hmac = hash_hmac('sha1', $sign_str, $this->SecretKey, true); + $sign = $this->AccessKey . ':' . $this->base64_urlSafeEncode($hmac); + + $header = [ + 'Authorization: QBox ' . $sign, + ]; + if ($body) { + $header[] = 'Content-Type: application/json'; + } + return $this->curl($method, $url, $body, $header); + } + + private function base64_urlSafeEncode($data) + { + $find = array('+', '/'); + $replace = array('-', '_'); + return str_replace($find, $replace, base64_encode($data)); + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_USERAGENT, 'QiniuPHP/7.14.0 (' . php_uname("s") . '/' . php_uname("m") . ') PHP/' . phpversion()); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($errno) { + curl_close($ch); + throw new Exception('Curl error: ' . curl_error($ch)); + } + curl_close($ch); + + if ($httpCode == 200) { + $arr = json_decode($response, true); + if($arr) return $arr; + return true; + } else { + $arr = json_decode($response, true); + if ($arr && !empty($arr['error'])) { + throw new Exception($arr['error']); + } else { + throw new Exception('返回数据解析失败'); + } + } + } +} diff --git a/app/lib/client/TencentCloud.php b/app/lib/client/TencentCloud.php new file mode 100644 index 0000000..a167b30 --- /dev/null +++ b/app/lib/client/TencentCloud.php @@ -0,0 +1,127 @@ +SecretId = $SecretId; + $this->SecretKey = $SecretKey; + $this->endpoint = $endpoint; + $this->service = $service; + $this->version = $version; + $this->region = $region; + } + + /** + * @param string $action 方法名称 + * @param array $param 请求参数 + * @return array + * @throws Exception + */ + public function request($action, $param) + { + $param = array_filter($param, function ($a) { return $a !== null;}); + if (!$param) $param = (object)[]; + $payload = json_encode($param); + $time = time(); + $authorization = $this->generateSign($payload, $time); + $header = [ + 'Authorization: '.$authorization, + 'Content-Type: application/json; charset=utf-8', + 'X-TC-Action: '.$action, + 'X-TC-Timestamp: '.$time, + 'X-TC-Version: '.$this->version, + ]; + if($this->region) { + $header[] = 'X-TC-Region: '.$this->region; + } + $res = $this->curl_post($payload, $header); + return $res; + } + + private function generateSign($payload, $time) + { + $algorithm = "TC3-HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = "POST"; + $canonicalUri = "/"; + $canonicalQueryString = ""; + $canonicalHeaders = "content-type:application/json; charset=utf-8\n"."host:".$this->endpoint."\n"; + $signedHeaders = "content-type;host"; + $hashedRequestPayload = hash("SHA256", $payload); + $canonicalRequest = $httpRequestMethod."\n" + .$canonicalUri."\n" + .$canonicalQueryString."\n" + .$canonicalHeaders."\n" + .$signedHeaders."\n" + .$hashedRequestPayload; + + // step 2: build string to sign + $date = gmdate("Y-m-d", $time); + $credentialScope = $date."/".$this->service."/tc3_request"; + $hashedCanonicalRequest = hash("SHA256", $canonicalRequest); + $stringToSign = $algorithm."\n" + .$time."\n" + .$credentialScope."\n" + .$hashedCanonicalRequest; + + // step 3: sign string + $secretDate = hash_hmac("SHA256", $date, "TC3".$this->SecretKey, true); + $secretService = hash_hmac("SHA256", $this->service, $secretDate, true); + $secretSigning = hash_hmac("SHA256", "tc3_request", $secretService, true); + $signature = hash_hmac("SHA256", $stringToSign, $secretSigning); + + // step 4: build authorization + $authorization = $algorithm + ." Credential=".$this->SecretId."/".$credentialScope + .", SignedHeaders=content-type;host, Signature=".$signature; + + return $authorization; + } + + private function curl_post($payload, $header) + { + $url = 'https://'.$this->endpoint.'/'; + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + curl_close($ch); + throw new Exception('Curl error: ' . curl_error($ch)); + } + curl_close($ch); + + $arr = json_decode($response, true); + if ($arr) { + if (isset($arr['Response']['Error'])) { + throw new Exception($arr['Response']['Error']['Message']); + } else { + return $arr['Response']; + } + } else { + throw new Exception('返回数据解析失败'); + } + } +} \ No newline at end of file diff --git a/app/lib/client/Ucloud.php b/app/lib/client/Ucloud.php new file mode 100644 index 0000000..4d1a9d7 --- /dev/null +++ b/app/lib/client/Ucloud.php @@ -0,0 +1,51 @@ +PublicKey = $PublicKey; + $this->PrivateKey = $PrivateKey; + } + + public function request($action, $params) + { + $param = [ + 'Action' => $action, + 'PublicKey' => $this->PublicKey, + ]; + $param = array_merge($param, $params); + $param['Signature'] = $this->ucloudSignature($param); + $ua = sprintf("PHP/%s PHP-SDK/%s", phpversion(), self::VERSION); + $response = get_curl($this->ApiUrl, json_encode($param), 0, 0, 0, $ua, 0, ['Content-Type: application/json']); + $result = json_decode($response, true); + if (isset($result['RetCode']) && $result['RetCode'] == 0) { + return $result; + } elseif (isset($result['Message'])) { + throw new Exception($result['Message']); + } else { + throw new Exception('返回数据解析失败'); + } + } + + private function ucloudSignature($param) + { + ksort($param); + $str = ''; + foreach ($param as $key => $value) { + $str .= $key . $value; + } + $str .= $this->PrivateKey; + return sha1($str); + } +} diff --git a/app/lib/client/Volcengine.php b/app/lib/client/Volcengine.php new file mode 100644 index 0000000..928e599 --- /dev/null +++ b/app/lib/client/Volcengine.php @@ -0,0 +1,192 @@ +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->endpoint = $endpoint; + $this->service = $service; + $this->version = $version; + $this->region = $region; + } + + /** + * @param string $method 请求方法 + * @param string $action 方法名称 + * @param array $params 请求参数 + * @return array + * @throws Exception + */ + public function request($method, $action, $params = [], $querys = []) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { return $a !== null;}); + } + + $query = [ + 'Action' => $action, + 'Version' => $this->version, + ]; + + $body = ''; + if ($method == 'GET') { + $query = array_merge($query, $params); + } else { + $body = !empty($params) ? json_encode($params) : ''; + if (!empty($querys)) { + $query = array_merge($query, $querys); + } + } + + $time = time(); + $headers = [ + 'Host' => $this->endpoint, + 'X-Date' => gmdate("Ymd\THis\Z", $time), + //'X-Content-Sha256' => hash("sha256", $body), + ]; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + $path = '/'; + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path . '?' . http_build_query($query); + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($method, $path, $query, $headers, $body, $time) + { + $algorithm = "HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $path; + if (substr($canonicalUri, -1) != "/") $canonicalUri .= "/"; + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + $canonicalRequest = $httpRequestMethod . "\n" + . $canonicalUri . "\n" + . $canonicalQueryString . "\n" + . $canonicalHeaders . "\n" + . $signedHeaders . "\n" + . $hashedRequestPayload; + + // step 2: build string to sign + $date = gmdate("Ymd\THis\Z", $time); + $shortDate = substr($date, 0, 8); + $credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/request'; + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = $algorithm . "\n" + . $date . "\n" + . $credentialScope . "\n" + . $hashedCanonicalRequest; + + // step 3: sign string + $kDate = hash_hmac("sha256", $shortDate, $this->SecretAccessKey, true); + $kRegion = hash_hmac("sha256", $this->region, $kDate, true); + $kService = hash_hmac("sha256", $this->service, $kRegion, true); + $kSigning = hash_hmac("sha256", "request", $kService, true); + $signature = hash_hmac("sha256", $stringToSign, $kSigning); + + // step 4: build authorization + $credential = $this->AccessKeyId . '/' . $credentialScope; + $authorization = $algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + private function curl($method, $url, $body, $header) + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno) { + curl_close($ch); + throw new Exception('Curl error: ' . curl_error($ch)); + } + curl_close($ch); + + $arr = json_decode($response, true); + if ($arr) { + if (isset($arr['ResponseMetadata']['Error']['MessageCN'])) { + throw new Exception($arr['ResponseMetadata']['Error']['MessageCN']); + } elseif (isset($arr['ResponseMetadata']['Error']['Message'])) { + throw new Exception($arr['ResponseMetadata']['Error']['Message']); + } elseif (isset($arr['Result'])) { + return $arr['Result']; + } else { + return true; + } + } else { + throw new Exception('返回数据解析失败'); + } + } +} diff --git a/app/lib/deploy/aliyun.php b/app/lib/deploy/aliyun.php new file mode 100644 index 0000000..66d2b0a --- /dev/null +++ b/app/lib/deploy/aliyun.php @@ -0,0 +1,649 @@ +AccessKeyId = $config['AccessKeyId']; + $this->AccessKeySecret = $config['AccessKeySecret']; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) throw new Exception('必填参数不能为空'); + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'cas.aliyuncs.com', '2020-04-07'); + $param = ['Action' => 'ListUserCertificateOrder']; + $client->request($param); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if ($config['product'] == 'api') { + $this->deploy_api($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'vod') { + $this->deploy_vod($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'fc') { + $this->deploy_fc($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'fc2') { + $this->deploy_fc2($fullchain, $privatekey, $config); + } else { + [$cert_id, $cert_name] = $this->get_cert_id($fullchain, $privatekey); + if (!$cert_id) throw new Exception('证书ID获取失败'); + if ($config['product'] == 'cdn') { + $this->deploy_cdn($cert_id, $cert_name, $config); + } elseif ($config['product'] == 'dcdn') { + $this->deploy_dcdn($cert_id, $cert_name, $config); + } elseif ($config['product'] == 'esa') { + $this->deploy_esa($cert_id, $config); + } elseif ($config['product'] == 'oss') { + $this->deploy_oss($cert_id, $config); + } elseif ($config['product'] == 'waf') { + $this->deploy_waf($cert_id, $config); + } elseif ($config['product'] == 'waf2') { + $this->deploy_waf2($cert_id, $config); + } elseif ($config['product'] == 'ddoscoo') { + $this->deploy_ddoscoo($cert_id, $config); + } elseif ($config['product'] == 'live') { + $this->deploy_live($cert_id, $cert_name, $config); + } elseif ($config['product'] == 'clb') { + $this->deploy_clb($cert_id, $cert_name, $config); + } elseif ($config['product'] == 'alb') { + $this->deploy_alb($cert_id, $config); + } elseif ($config['product'] == 'nlb') { + $this->deploy_nlb($cert_id, $config); + } else { + throw new Exception('未知的产品类型'); + } + $info['cert_id'] = $cert_id; + $info['cert_name'] = $cert_name; + } + } + + private function get_cert_id($fullchain, $privatekey) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + $serial_no = strtolower($certInfo['serialNumberHex']); + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'cas.aliyuncs.com', '2020-04-07'); + $param = [ + 'Action' => 'ListUserCertificateOrder', + 'Keyword' => $certInfo['subject']['CN'], + 'OrderType' => 'UPLOAD', + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('查询证书列表失败:' . $e->getMessage()); + } + $cert_id = null; + if ($data['TotalCount'] > 0 && !empty($data['CertificateOrderList'])) { + foreach ($data['CertificateOrderList'] as $cert) { + if (strtolower($cert['SerialNo']) == $serial_no) { + $cert_id = $cert['CertificateId']; + $cert_name = $cert['Name']; + break; + } + } + } + if ($cert_id) { + $this->log('找到已上传的证书 CertId=' . $cert_id); + return [$cert_id, $cert_name]; + } + + $param = [ + 'Action' => 'UploadUserCertificate', + 'Name' => $cert_name, + 'Cert' => $fullchain, + 'Key' => $privatekey, + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $this->log('证书上传成功!CertId=' . $data['CertId']); + usleep(500000); + return [$data['CertId'], $cert_name]; + } + + private function deploy_cdn($cert_id, $cert_name, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('CDN绑定域名不能为空'); + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'cdn.aliyuncs.com', '2018-05-10'); + $param = [ + 'Action' => 'SetCdnDomainSSLCertificate', + 'DomainName' => $domain, + 'CertName' => $cert_name, + 'CertType' => 'cas', + 'SSLProtocol' => 'on', + 'CertId' => $cert_id, + ]; + $client->request($param); + $this->log('CDN域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_dcdn($cert_id, $cert_name, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('DCDN绑定域名不能为空'); + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'dcdn.aliyuncs.com', '2018-01-15'); + $param = [ + 'Action' => 'SetDcdnDomainSSLCertificate', + 'DomainName' => $domain, + 'CertName' => $cert_name, + 'CertType' => 'cas', + 'SSLProtocol' => 'on', + 'CertId' => $cert_id, + ]; + $client->request($param); + $this->log('DCDN域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_esa($cas_id, $config) + { + $sitename = $config['esa_sitename']; + if (empty($sitename)) throw new Exception('ESA站点名称不能为空'); + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'esa.cn-hangzhou.aliyuncs.com', '2024-09-10'); + $param = [ + 'Action' => 'ListSites', + 'SiteName' => $sitename, + 'SiteSearchType' => 'exact', + ]; + try { + $data = $client->request($param, 'GET'); + } catch (Exception $e) { + throw new Exception('查询ESA站点列表失败:' . $e->getMessage()); + } + if ($data['TotalCount'] == 0) throw new Exception('ESA站点 ' . $sitename . ' 不存在'); + $this->log('成功查询到' . $data['TotalCount'] . '个ESA站点'); + $site_id = $data['Sites'][0]['SiteId']; + + $param = [ + 'Action' => 'ListCertificates', + 'SiteId' => $site_id, + ]; + try { + $data = $client->request($param, 'GET'); + } catch (Exception $e) { + throw new Exception('查询ESA站点' . $sitename . '证书列表失败:' . $e->getMessage()); + } + $this->log('ESA站点 ' . $sitename . ' 查询到' . $data['TotalCount'] . '个SSL证书'); + + $cert_id = null; + $cert_name = null; + foreach ($data['Result'] as $cert) { + $domains = explode(',', $cert['SAN']); + $flag = true; + foreach ($domains as $domain) { + if (!in_array($domain, $config['domainList'])) { + $flag = false; + break; + } + } + if ($flag) { + $cert_id = $cert['Id']; + $cert_name = $cert['CommonName']; + break; + } + } + + $param = [ + 'Action' => 'SetCertificate', + 'SiteId' => $site_id, + 'Type' => 'cas', + 'CasId' => $cas_id, + ]; + if ($cert_id) { + $param['Update'] = 'true'; + $param['Id'] = $cert_id; + } + $client->request($param); + if ($cert_id) { + $this->log('ESA站点 ' . $sitename . ' 域名 ' . $cert_name . ' 更新证书成功!'); + } else { + $this->log('ESA站点 ' . $sitename . ' 添加证书成功!'); + } + } + + private function deploy_oss($cert_id, $config) + { + if (empty($config['domain'])) throw new Exception('OSS绑定域名不能为空'); + if (empty($config['oss_endpoint'])) throw new Exception('OSS Endpoint不能为空'); + if (empty($config['oss_bucket'])) throw new Exception('OSS Bucket不能为空'); + $client = new AliyunOSS($this->AccessKeyId, $this->AccessKeySecret, $config['oss_endpoint']); + $client->addBucketCnameCert($config['oss_bucket'], $config['domain'], $cert_id); + $this->log('OSS域名 ' . $config['domain'] . ' 部署证书成功!'); + } + + private function deploy_waf($cert_id, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('WAF绑定域名不能为空'); + + $endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com'; + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2021-10-01'); + + $param = [ + 'Action' => 'DescribeInstance', + 'RegionId' => $config['region'], + ]; + try { + $data = $client->request($param, 'GET'); + } catch (Exception $e) { + throw new Exception('获取WAF实例详情失败:' . $e->getMessage()); + } + if (empty($data['InstanceId'])) throw new Exception('当前账号未找到WAF实例'); + $instance_id = $data['InstanceId']; + $this->log('获取WAF实例ID成功 InstanceId=' . $instance_id); + + $param = [ + 'Action' => 'DescribeDomainDetail', + 'InstanceId' => $instance_id, + 'Domain' => $domain, + 'RegionId' => $config['region'], + ]; + try { + $data = $client->request($param, 'GET'); + } catch (Exception $e) { + throw new Exception('查询CNAME接入详情失败:' . $e->getMessage()); + } + + if (isset($data['Listen']['CertId'])) { + $old_cert_id = $data['Listen']['CertId']; + if (strpos($old_cert_id, '-')) $old_cert_id = substr($old_cert_id, 0, strpos($old_cert_id, '-')); + if (!empty($old_cert_id) && $old_cert_id == $cert_id) { + $this->log('WAF域名 ' . $domain . ' 证书已配置,无需重复操作'); + return; + } + } + + $data['Listen']['CertId'] = $cert_id . '-cn-hangzhou'; + if (empty($data['Listen']['HttpsPorts'])) $data['Listen']['HttpsPorts'] = [443]; + $data['Redirect']['Backends'] = $data['Redirect']['AllBackends']; + $param = [ + 'Action' => 'ModifyDomain', + 'InstanceId' => $instance_id, + 'Domain' => $domain, + 'Listen' => json_encode($data['Listen']), + 'Redirect' => json_encode($data['Redirect']), + 'RegionId' => $config['region'], + ]; + $data = $client->request($param); + + $this->log('WAF域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_waf2($cert_id, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('WAF绑定域名不能为空'); + + $endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com'; + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2019-09-10'); + + $param = [ + 'Action' => 'DescribeInstanceInfo', + 'RegionId' => $config['region'], + ]; + try { + $data = $client->request($param, 'GET'); + } catch (Exception $e) { + throw new Exception('获取WAF实例详情失败:' . $e->getMessage()); + } + if (empty($data['InstanceInfo']['InstanceId'])) throw new Exception('当前账号未找到WAF实例'); + $instance_id = $data['InstanceInfo']['InstanceId']; + $this->log('获取WAF实例ID成功 InstanceId=' . $instance_id); + + $param = [ + 'Action' => 'CreateCertificateByCertificateId', + 'InstanceId' => $instance_id, + 'Domain' => $domain, + 'CertificateId' => $cert_id, + ]; + $client->request($param); + + $this->log('WAF域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_api($fullchain, $privatekey, $config) + { + $domain = $config['domain']; + $groupid = $config['api_groupid']; + if (empty($groupid)) throw new Exception('API分组ID不能为空'); + if (empty($domain)) throw new Exception('API分组绑定域名不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $endpoint = 'apigateway.' . $config['regionid'] . '.aliyuncs.com'; + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2016-07-14'); + + $param = [ + 'Action' => 'SetDomainCertificate', + 'GroupId' => $groupid, + 'DomainName' => $domain, + 'CertificateName' => $cert_name, + 'CertificateBody' => $fullchain, + 'CertificatePrivateKey' => $privatekey, + ]; + $client->request($param); + + $this->log('API网关域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_ddoscoo($cert_id, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('绑定域名不能为空'); + + $endpoint = 'ddoscoo.' . $config['region'] . '.aliyuncs.com'; + + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2020-01-01'); + + $param = [ + 'Action' => 'AssociateWebCert', + 'Domain' => $domain, + 'CertId' => $cert_id, + ]; + $client->request($param); + + $this->log('DDoS高防域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_live($cert_id, $cert_name, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('视频直播绑定域名不能为空'); + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'live.aliyuncs.com', '2016-11-01'); + $param = [ + 'Action' => 'SetLiveDomainCertificate', + 'DomainName' => $domain, + 'CertName' => $cert_name, + 'CertType' => 'cas', + 'SSLProtocol' => 'on', + 'CertId' => $cert_id, + ]; + $client->request($param); + $this->log('设置视频直播域名 ' . $domain . ' 证书成功!'); + } + + private function deploy_vod($fullchain, $privatekey, $config) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('视频点播绑定域名不能为空'); + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'vod.cn-shanghai.aliyuncs.com', '2017-03-21'); + $param = [ + 'Action' => 'SetVodDomainCertificate', + 'DomainName' => $domain, + 'SSLProtocol' => 'on', + 'SSLPub' => $fullchain, + 'SSLPri' => $privatekey, + ]; + $client->request($param); + $this->log('视频点播域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_fc($fullchain, $privatekey, $config) + { + $domain = $config['domain']; + $fc_cname = $config['fc_cname']; + if (empty($domain)) throw new Exception('函数计算域名不能为空'); + if (empty($fc_cname)) throw new Exception('域名CNAME地址不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new AliyunNewClient($this->AccessKeyId, $this->AccessKeySecret, $fc_cname, '2023-03-30'); + + try { + $data = $client->request('GET', 'GetCustomDomain', '/2023-03-30/custom-domains/' . $domain); + } catch (Exception $e) { + throw new Exception('获取绑定域名信息失败:' . $e->getMessage()); + } + $this->log('获取函数计算绑定域名信息成功'); + + if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) { + $this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作'); + return; + } + + if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS'; + $data['certConfig']['certName'] = $cert_name; + $data['certConfig']['certificate'] = $fullchain; + $data['certConfig']['privateKey'] = $privatekey; + + $param = [ + 'authConfig' => $data['authConfig'], + 'certConfig' => $data['certConfig'], + 'protocol' => $data['protocol'], + 'routeConfig' => $data['routeConfig'], + 'tlsConfig' => $data['tlsConfig'], + 'wafConfig' => $data['wafConfig'], + ]; + $client->request('PUT', 'UpdateCustomDomain', '/2023-03-30/custom-domains/' . $domain, $param); + + $this->log('函数计算域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_fc2($fullchain, $privatekey, $config) + { + $domain = $config['domain']; + $fc_cname = $config['fc_cname']; + if (empty($domain)) throw new Exception('函数计算域名不能为空'); + if (empty($fc_cname)) throw new Exception('域名CNAME地址不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new AliyunNewClient($this->AccessKeyId, $this->AccessKeySecret, $fc_cname, '2021-04-06'); + + try { + $data = $client->request('GET', 'GetCustomDomain', '/2021-04-06/custom-domains/' . $domain); + } catch (Exception $e) { + throw new Exception('获取绑定域名信息失败:' . $e->getMessage()); + } + $this->log('获取函数计算绑定域名信息成功'); + + if (isset($data['certConfig']['certificate']) && $data['certConfig']['certificate'] == $fullchain) { + $this->log('函数计算域名 ' . $domain . ' 证书已配置,无需重复操作'); + return; + } + + if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS'; + $data['certConfig']['certName'] = $cert_name; + $data['certConfig']['certificate'] = $fullchain; + $data['certConfig']['privateKey'] = $privatekey; + + $param = [ + 'protocol' => $data['protocol'], + 'routeConfig' => $data['routeConfig'], + 'certConfig' => $data['certConfig'], + 'tlsConfig' => $data['tlsConfig'], + 'wafConfig' => $data['wafConfig'], + ]; + $client->request('PUT', 'UpdateCustomDomain', '/2021-04-06/custom-domains/' . $domain, $param); + + $this->log('函数计算域名 ' . $domain . ' 部署证书成功!'); + } + + private function deploy_clb($cert_id, $cert_name, $config) + { + if (empty($config['clb_id'])) throw new Exception('负载均衡实例ID不能为空'); + if (empty($config['clb_port'])) throw new Exception('HTTPS监听端口不能为空'); + + $endpoint = 'slb.' . $config['regionid'] . '.aliyuncs.com'; + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2014-05-15'); + + $param = [ + 'Action' => 'DescribeServerCertificates', + 'RegionId' => $config['regionid'], + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('获取服务器证书列表失败:' . $e->getMessage()); + } + + $ServerCertificateId = null; + foreach ($data['ServerCertificates']['ServerCertificate'] as $cert) { + if ($cert['IsAliCloudCertificate'] == 1 && $cert['AliCloudCertificateId'] == $cert_id) { + $ServerCertificateId = $cert['ServerCertificateId']; + break; + } + } + if (!$ServerCertificateId) { + $param = [ + 'Action' => 'UploadServerCertificate', + 'RegionId' => $config['regionid'], + 'AliCloudCertificateId' => $cert_id, + 'AliCloudCertificateName' => $cert_name, + 'AliCloudCertificateRegionId' => 'cn-hangzhou', + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('服务器证书添加失败:' . $e->getMessage()); + } + $ServerCertificateId = $data['ServerCertificateId']; + $this->log('服务器证书添加成功 ServerCertificateId=' . $ServerCertificateId); + } else { + $this->log('找到已添加的服务器证书 ServerCertificateId=' . $ServerCertificateId); + } + + $param = [ + 'Action' => 'DescribeLoadBalancerHTTPSListenerAttribute', + 'RegionId' => $config['regionid'], + 'LoadBalancerId' => $config['clb_id'], + 'ListenerPort' => $config['clb_port'], + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('HTTPS监听配置查询失败:' . $e->getMessage()); + } + + if ($data['ServerCertificateId'] == $ServerCertificateId) { + $this->log('负载均衡HTTPS监听已配置该证书,无需重复操作'); + return; + } + + $param = [ + 'Action' => 'SetLoadBalancerHTTPSListenerAttribute', + 'RegionId' => $config['regionid'], + 'LoadBalancerId' => $config['clb_id'], + 'ListenerPort' => $config['clb_port'], + ]; + $keys = ['Bandwidth', 'XForwardedFor', 'Scheduler', 'StickySession', 'StickySessionType', 'CookieTimeout', 'Cookie', 'HealthCheck', 'HealthCheckMethod', 'HealthCheckDomain', 'HealthCheckURI', 'HealthyThreshold', 'UnhealthyThreshold', 'HealthCheckTimeout', 'HealthCheckInterval', 'HealthCheckConnectPort', 'HealthCheckHttpCode', 'ServerCertificateId', 'CACertificateId', 'VServerGroup', 'VServerGroupId', 'XForwardedFor_SLBIP', 'XForwardedFor_SLBID', 'XForwardedFor_proto', 'Gzip', 'AclId', 'AclType', 'AclStatus', 'IdleTimeout', 'RequestTimeout', 'EnableHttp2', 'TLSCipherPolicy', 'Description', 'XForwardedFor_SLBPORT', 'XForwardedFor_ClientSrcPort']; + foreach ($keys as $key) { + if (isset($data[$key])) $param[$key] = $data[$key]; + } + $param['ServerCertificateId'] = $ServerCertificateId; + $client->request($param); + $this->log('负载均衡HTTPS监听证书配置成功!'); + } + + private function deploy_alb($cert_id, $config) + { + if (empty($config['alb_listener_id'])) throw new Exception('负载均衡监听ID不能为空'); + + $endpoint = 'alb.' . $config['regionid'] . '.aliyuncs.com'; + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2020-06-16'); + + $param = [ + 'Action' => 'ListListenerCertificates', + 'MaxResults' => 100, + 'ListenerId' => $config['alb_listener_id'], + 'CertificateType' => 'Server', + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('获取监听证书列表失败:' . $e->getMessage()); + } + foreach ($data['Certificates'] as $cert) { + if (strpos($cert['CertificateId'], '-')) $cert['CertificateId'] = substr($cert['CertificateId'], 0, strpos($cert['CertificateId'], '-')); + if ($cert['CertificateId'] == $cert_id) { + $this->log('负载均衡监听证书已添加,无需重复操作'); + return; + } + } + + $param = [ + 'Action' => 'AssociateAdditionalCertificatesWithListener', + 'ListenerId' => $config['alb_listener_id'], + 'Certificates.1.CertificateId' => $cert_id . '-cn-hangzhou', + ]; + $client->request($param); + $this->log('应用型负载均衡监听证书添加成功!'); + } + + private function deploy_nlb($cert_id, $config) + { + if (empty($config['nlb_listener_id'])) throw new Exception('负载均衡监听ID不能为空'); + + $endpoint = 'nlb.' . $config['regionid'] . '.aliyuncs.com'; + $client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2022-04-30'); + + $param = [ + 'Action' => 'ListListenerCertificates', + 'MaxResults' => 50, + 'ListenerId' => $config['nlb_listener_id'], + 'CertificateType' => 'Server', + ]; + try { + $data = $client->request($param); + } catch (Exception $e) { + throw new Exception('获取监听证书列表失败:' . $e->getMessage()); + } + foreach ($data['Certificates'] as $cert) { + if (strpos($cert['CertificateId'], '-')) $cert['CertificateId'] = substr($cert['CertificateId'], 0, strpos($cert['CertificateId'], '-')); + if ($cert['CertificateId'] == $cert_id) { + $this->log('负载均衡监听证书已添加,无需重复操作'); + return; + } + } + + $param = [ + 'Action' => 'AssociateAdditionalCertificatesWithListener', + 'ListenerId' => $config['nlb_listener_id'], + 'AdditionalCertificateIds.1' => $cert_id . '-cn-hangzhou', + ]; + $client->request($param); + $this->log('网络型负载均衡监听证书添加成功!'); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/allwaf.php b/app/lib/deploy/allwaf.php new file mode 100644 index 0000000..515bc8c --- /dev/null +++ b/app/lib/deploy/allwaf.php @@ -0,0 +1,127 @@ +accessKeyId = $config['accessKeyId']; + $this->accessKey = $config['accessKey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->accessKeyId) || empty($this->accessKey)) throw new Exception('必填参数不能为空'); + $this->getAccessToken(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domains = $config['domainList']; + if (empty($domains)) throw new Exception('没有设置要部署的域名'); + + $this->getAccessToken(); + + $params = [ + 'domains' => $domains, + 'offset' => 0, + 'size' => 10, + ]; + try { + $data = $this->request('/SSLCertService/listSSLCerts', $params); + } catch (Exception $e) { + throw new Exception('获取证书列表失败:' . $e->getMessage()); + } + $list = json_decode(base64_decode($data['sslCertsJSON']), true); + if (!$list || empty($list)) { + throw new Exception('证书列表为空'); + } + $this->log('获取证书列表成功(total=' . count($list) . ')'); + + $certInfo = openssl_x509_parse($fullchain, true); + + foreach ($list as $row) { + $params = [ + 'sslCertId' => $row['id'], + 'isOn' => true, + 'name' => $row['name'], + 'description' => $row['description'], + 'serverName' => $row['serverName'], + 'isCA' => false, + 'certData' => base64_encode($fullchain), + 'keyData' => base64_encode($privatekey), + 'timeBeginAt' => $certInfo['validFrom_time_t'], + 'timeEndAt' => $certInfo['validTo_time_t'], + 'dnsNames' => $domains, + 'commonNames' => [$certInfo['issuer']['CN']], + ]; + $this->request('/SSLCertService/updateSSLCert', $params); + $this->log('证书ID:' . $row['id'] . '更新成功!'); + } + } + + private function getAccessToken() + { + $path = '/APIAccessTokenService/getAPIAccessToken'; + $params = [ + 'type' => $this->usertype, + 'accessKeyId' => $this->accessKeyId, + 'accessKey' => $this->accessKey, + ]; + $result = $this->request($path, $params); + if (isset($result['token'])) { + $this->accessToken = $result['token']; + } else { + throw new Exception('登录成功,获取AccessToken失败'); + } + } + + private function request($path, $params = null) + { + $url = $this->url . $path; + $headers = []; + $body = null; + if ($this->accessToken) { + $headers[] = 'X-Cloud-Access-Token: ' . $this->accessToken; + } + if ($params) { + $headers[] = 'Content-Type: application/json'; + $body = json_encode($params); + } + $response = curl_client($url, $body, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 200) { + return isset($result['data']) ? $result['data'] : null; + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + if (!empty($response['body'])) $this->log('Response:' . $response['body']); + throw new Exception('返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/aws.php b/app/lib/deploy/aws.php new file mode 100644 index 0000000..e783212 --- /dev/null +++ b/app/lib/deploy/aws.php @@ -0,0 +1,90 @@ +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'iam.amazonaws.com', 'iam','2010-05-08', 'us-east-1'); + $client->requestXml('GET', 'GetUser'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['distribution_id'])) throw new Exception('分配ID不能为空'); + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + if(isset($info['cert_id']) && isset($info['cert_name']) && $info['cert_name'] == $config['cert_name']){ + $cert_id = $info['cert_id']; + $this->log('证书已上传:' . $cert_id); + }else{ + $cert_id = $this->get_cert_id($fullchain, $privatekey); + $this->log('证书上传成功:' . $cert_id); + $info['cert_id'] = $cert_id; + $info['cert_name'] = $config['cert_name']; + usleep(500000); + } + + $client = new \app\lib\client\AWS($this->AccessKeyId, $this->SecretAccessKey, 'cloudfront.amazonaws.com', 'cloudfront', '2020-05-31', 'us-east-1'); + try{ + $data = $client->requestXmlN('GET', '/distribution/'.$config['distribution_id'].'/config', [], null, true); + }catch(Exception $e){ + throw new Exception('获取分配信息失败:'.$e->getMessage()); + } + + $data['ViewerCertificate']['ACMCertificateArn'] = $cert_id; + $data['ViewerCertificate']['CloudFrontDefaultCertificate'] = false; + $xml = new \SimpleXMLElement(''); + $client->requestXmlN('PUT', '/distribution/'.$config['distribution_id'].'/config', $data, $xml); + $this->log('分配ID: ' . $config['distribution_id'] . ' 证书部署成功!'); + } + + private function get_cert_id($fullchain, $privatekey) + { + $cert = explode('-----END CERTIFICATE-----', $fullchain)[0] . '-----END CERTIFICATE-----'; + $param = [ + 'Certificate' => base64_encode($cert), + 'PrivateKey' => base64_encode($privatekey), + ]; + + $client = new \app\lib\client\AWS($this->AccessKeyId, $this->SecretAccessKey, 'acm.us-east-1.amazonaws.com', 'acm', '', 'us-east-1'); + try{ + $data = $client->request('POST', 'CertificateManager.ImportCertificate', $param); + $cert_id = $data['CertificateArn']; + }catch(Exception $e){ + throw new Exception('上传证书失败:'.$e->getMessage()); + } + return $cert_id; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/baidu.php b/app/lib/deploy/baidu.php new file mode 100644 index 0000000..89140a6 --- /dev/null +++ b/app/lib/deploy/baidu.php @@ -0,0 +1,71 @@ +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'cdn.baidubce.com'); + $client->request('GET', '/v2/domain'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, 'cdn.baidubce.com'); + try{ + $data = $client->request('GET', '/v2/'.$config['domain'].'/certificates'); + if(isset($data['certName']) && $data['certName'] == $config['cert_name']){ + $this->log('CDN域名 ' . $config['domain'] . ' 证书已存在,无需重复部署'); + return; + } + }catch(Exception $e){ + $this->log($e->getMessage()); + } + + $param = [ + 'httpsEnable' => 'ON', + 'certificate' => [ + 'certName' => $config['cert_name'], + 'certServerData' => $fullchain, + 'certPrivateData' => $privatekey, + ], + ]; + $data = $client->request('PUT', '/v2/'.$config['domain'].'/certificates', null, $param); + $info['cert_id'] = $data['certId']; + $this->log('CDN域名 ' . $config['domain'] . ' 证书部署成功!'); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/baishan.php b/app/lib/deploy/baishan.php new file mode 100644 index 0000000..de94f51 --- /dev/null +++ b/app/lib/deploy/baishan.php @@ -0,0 +1,85 @@ +token = $config['token']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->token)) throw new Exception('token不能为空'); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['id'])) throw new Exception('证书ID不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $params = [ + 'cert_id' => $config['id'], + 'name' => $cert_name, + 'certificate' => $fullchain, + 'key' => $privatekey, + ]; + try { + $this->request('/v2/domain/certificate?token=' . $this->token, $params); + } catch (Exception $e) { + if (strpos($e->getMessage(), 'this certificate is exists') !== false) { + $this->log('证书ID:' . $config['id'] . '已存在,无需更新'); + return; + } + throw new Exception($e->getMessage()); + } + + $this->log('证书ID:' . $config['id'] . '更新成功!'); + } + + private function request($path, $params = null) + { + $url = $this->url . $path; + $headers = []; + $body = null; + if ($params) { + $headers[] = 'Content-Type: application/json'; + $body = json_encode($params); + } + $response = curl_client($url, $body, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 0) { + return $result; + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + if (!empty($response['body'])) $this->log('Response:' . $response['body']); + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/btpanel.php b/app/lib/deploy/btpanel.php new file mode 100644 index 0000000..596bcac --- /dev/null +++ b/app/lib/deploy/btpanel.php @@ -0,0 +1,126 @@ +url = rtrim($config['url'], '/'); + $this->key = $config['key']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥'); + + $path = '/config?action=get_config'; + $response = $this->request($path, []); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status'] == 1) { + return true; + } else { + throw new Exception(isset($result['msg']) ? $result['msg'] : '面板地址无法连接'); + } + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if ($config['type'] == '1') { + $this->deployPanel($fullchain, $privatekey); + $this->log("面板证书部署成功"); + return; + } + $sites = explode("\n", $config['sites']); + $success = 0; + $errmsg = null; + foreach ($sites as $site) { + $siteName = trim($site); + if (empty($siteName)) continue; + try { + $this->deploySite($siteName, $fullchain, $privatekey); + $this->log("网站 {$siteName} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("网站 {$siteName} 证书部署失败:" . $errmsg); + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '要部署的网站不存在'); + } + } + + private function deployPanel($fullchain, $privatekey) + { + $path = '/config?action=SavePanelSSL'; + $data = [ + 'privateKey' => $privatekey, + 'certPem' => $fullchain, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + + private function deploySite($siteName, $fullchain, $privatekey) + { + $path = '/site?action=SetSSL'; + $data = [ + 'type' => '0', + 'siteName' => $siteName, + 'key' => $privatekey, + 'csr' => $fullchain, + ]; + $response = $this->request($path, $data); + $result = json_decode($response, true); + if (isset($result['status']) && $result['status']) { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ? $response : '返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($path, $params) + { + $url = $this->url . $path; + + $now_time = time(); + $post_data = [ + 'request_token' => md5($now_time . md5($this->key)), + 'request_time' => $now_time + ]; + $post_data = array_merge($post_data, $params); + $response = curl_client($url, $post_data, null, null, null, $this->proxy); + return $response['body']; + } +} diff --git a/app/lib/deploy/cachefly.php b/app/lib/deploy/cachefly.php new file mode 100644 index 0000000..a8ce28f --- /dev/null +++ b/app/lib/deploy/cachefly.php @@ -0,0 +1,67 @@ +apikey = $config['apikey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->apikey)) throw new Exception('API令牌不能为空'); + $this->request('/accounts/me'); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $params = [ + 'certificate' => $fullchain, + 'certificateKey' => $privatekey, + ]; + $this->request('/certificates', $params); + $this->log('证书上传成功!'); + } + + private function request($path, $params = null, $method = null) + { + $url = $this->url . $path; + $headers = ['x-cf-authorization: Bearer ' . $this->apikey]; + $body = null; + if ($params) { + $headers[] = 'Content-Type: application/json'; + $body = json_encode($params); + } + $response = curl_client($url, $body, null, null, $headers, $this->proxy, $method); + $result = json_decode($response['body'], true); + if ($response['code'] >= 200 && $response['code'] < 300) { + return $result; + } else { + if (!empty($response['body'])) $this->log('Response:' . $response['body']); + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/cdnfly.php b/app/lib/deploy/cdnfly.php new file mode 100644 index 0000000..82a8f53 --- /dev/null +++ b/app/lib/deploy/cdnfly.php @@ -0,0 +1,75 @@ +url = rtrim($config['url'], '/'); + $this->api_key = $config['api_key']; + $this->api_secret = $config['api_secret']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->api_key) || empty($this->api_secret)) throw new Exception('必填参数不能为空'); + $this->request('/v1/user'); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $id = $config['id']; + if (empty($id)) throw new Exception('证书ID不能为空'); + + $params = [ + 'type' => 'custom', + 'cert' => $fullchain, + 'key' => $privatekey, + ]; + $this->request('/v1/certs/' . $id, $params, 'PUT'); + $this->log("证书ID:{$id}更新成功!"); + } + + private function request($path, $params = null, $method = null) + { + $url = $this->url . $path; + $headers = ['api-key: ' . $this->api_key, 'api-secret: ' . $this->api_secret]; + $body = null; + if ($params) { + $headers[] = 'Content-Type: application/json'; + $body = json_encode($params); + } + $response = curl_client($url, $body, null, null, $headers, $this->proxy, $method); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 0) { + return isset($result['data']) ? $result['data'] : null; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception('返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/doge.php b/app/lib/deploy/doge.php new file mode 100644 index 0000000..93b1253 --- /dev/null +++ b/app/lib/deploy/doge.php @@ -0,0 +1,120 @@ +AccessKey = $config['AccessKey']; + $this->SecretKey = $config['SecretKey']; + } + + public function check() + { + if (empty($this->AccessKey) || empty($this->SecretKey)) throw new Exception('必填参数不能为空'); + $this->request('/cdn/cert/list.json'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('绑定的域名不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $cert_id = $this->get_cert_id($fullchain, $privatekey, $cert_name); + + $param = [ + 'id' => $cert_id, + 'domain' => $domain, + ]; + $this->request('/cdn/cert/bind.json', $param); + + $this->log('CDN域名 ' . $domain . ' 绑定证书成功!'); + $info['cert_id'] = $cert_id; + } + + private function get_cert_id($fullchain, $privatekey, $cert_name) + { + $cert_id = null; + + $data = $this->request('/cdn/cert/list.json'); + foreach ($data['certs'] as $cert) { + if ($cert_name == $cert['note']) { + $cert_id = $cert['id']; + $this->log('证书' . $cert_name . '已存在,证书ID:' . $cert_id); + } elseif ($cert['expire'] < time() && $cert['domainCount'] == 0) { + try { + $this->request('/cdn/cert/delete.json', ['id' => $cert['id']]); + $this->log('证书' . $cert['name'] . '已过期,删除证书成功'); + } catch (Exception $e) { + $this->log('证书' . $cert['name'] . '已过期,删除证书失败:' . $e->getMessage()); + } + usleep(300000); + } + } + + if (!$cert_id) { + $param = [ + 'note' => $cert_name, + 'cert' => $fullchain, + 'private' => $privatekey, + ]; + try { + $data = $this->request('/cdn/cert/upload.json', $param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $this->log('上传证书成功,证书ID:' . $data['id']); + $cert_id = $data['id']; + usleep(500000); + } + return $cert_id; + } + + private function request($path, $data = null, $json = false) + { + $body = null; + if($data){ + $body = $json ? json_encode($data) : http_build_query($data); + } + $signStr = $path . "\n" . $body; + $sign = hash_hmac('sha1', $signStr, $this->SecretKey); + $authorization = "TOKEN " . $this->AccessKey . ":" . $sign; + $headers = ['Authorization: ' . $authorization]; + if($body && $json) $headers[] = 'Content-Type: application/json'; + $url = 'https://api.dogecloud.com'.$path; + $response = curl_client($url, $body, null, null, $headers); + $result = json_decode($response['body'], true); + if(isset($result['code']) && $result['code'] == 200){ + return isset($result['data']) ? $result['data'] : true; + }elseif(isset($result['msg'])){ + throw new Exception($result['msg']); + }else{ + throw new Exception('请求失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/ftp.php b/app/lib/deploy/ftp.php new file mode 100644 index 0000000..fd0d3b8 --- /dev/null +++ b/app/lib/deploy/ftp.php @@ -0,0 +1,113 @@ +config = $config; + } + + public function check() + { + $this->connect(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $conn_id = $this->connect(); + ftp_pasv($conn_id, true); + if ($config['format'] == 'pem') { + $temp_stream = fopen('php://temp', 'r+'); + fwrite($temp_stream, $fullchain); + rewind($temp_stream); + if (ftp_fput($conn_id, $config['pem_cert_file'], $temp_stream, FTP_BINARY)) { + $this->log('证书文件上传成功:' . $config['pem_cert_file']); + } else { + fclose($temp_stream); + ftp_close($conn_id); + throw new Exception('证书文件上传失败:' . $config['pem_cert_file']); + } + fclose($temp_stream); + + $temp_stream = fopen('php://temp', 'r+'); + fwrite($temp_stream, $fullchain); + rewind($temp_stream); + if (ftp_fput($conn_id, $config['pem_key_file'], $temp_stream, FTP_BINARY)) { + $this->log('私钥文件上传成功:' . $config['pem_key_file']); + } else { + fclose($temp_stream); + ftp_close($conn_id); + throw new Exception('私钥文件上传失败:' . $config['pem_key_file']); + } + fclose($temp_stream); + } elseif ($config['format'] == 'pfx') { + $pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null); + + $temp_stream = fopen('php://temp', 'r+'); + fwrite($temp_stream, $pfx); + rewind($temp_stream); + if (ftp_fput($conn_id, $config['pfx_file'], $temp_stream, FTP_BINARY)) { + $this->log('PFX证书文件上传成功:' . $config['pfx_file']); + } else { + fclose($temp_stream); + ftp_close($conn_id); + throw new Exception('PFX证书文件上传失败:' . $config['pfx_file']); + } + fclose($temp_stream); + } + ftp_close($conn_id); + } + + private function connect() + { + if (!function_exists('ftp_connect')) { + throw new Exception('ftp扩展未安装'); + } + if (empty($this->config['host']) || empty($this->config['port']) || empty($this->config['username']) || empty($this->config['password'])) { + throw new Exception('必填参数不能为空'); + } + if (!filter_var($this->config['host'], FILTER_VALIDATE_IP) && !filter_var($this->config['host'], FILTER_VALIDATE_DOMAIN)) { + throw new Exception('主机地址不合法'); + } + if (!is_numeric($this->config['port']) || $this->config['port'] < 1 || $this->config['port'] > 65535) { + throw new Exception('端口不合法'); + } + + if($this->config['secure'] == '1'){ + $conn_id = ftp_ssl_connect($this->config['host'], intval($this->config['port']), 10); + if (!$conn_id) { + throw new Exception('FTP服务器无法连接(SSL)'); + } + }else{ + $conn_id = ftp_connect($this->config['host'], intval($this->config['port']), 10); + if (!$conn_id) { + throw new Exception('FTP服务器无法连接'); + } + } + if (!ftp_login($conn_id, $this->config['username'], $this->config['password'])) { + ftp_close($conn_id); + throw new Exception('FTP登录失败'); + } + return $conn_id; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + public function setLogger($logger) + { + $this->logger = $logger; + } +} diff --git a/app/lib/deploy/gcore.php b/app/lib/deploy/gcore.php new file mode 100644 index 0000000..c548a32 --- /dev/null +++ b/app/lib/deploy/gcore.php @@ -0,0 +1,77 @@ +apikey = $config['apikey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->apikey)) throw new Exception('API令牌不能为空'); + $this->request('/iam/clients/me'); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $id = $config['id']; + if (empty($id)) throw new Exception('证书ID不能为空'); + + $params = [ + 'name' => $config['name'], + 'sslCertificate' => $fullchain, + 'sslPrivateKey' => $privatekey, + 'validate_root_ca' => true, + ]; + $this->request('/cdn/sslData/' . $id, $params, 'PUT'); + $this->log('证书ID:' . $id . '更新成功!'); + } + + private function request($path, $params = null, $method = null) + { + $url = $this->url . $path; + $headers = ['Authorization: APIKey ' . $this->apikey]; + $body = null; + if ($params) { + $headers[] = 'Content-Type: application/json'; + $body = json_encode($params); + } + $response = curl_client($url, $body, null, null, $headers, $this->proxy, $method); + $result = json_decode($response['body'], true); + if ($response['code'] >= 200 && $response['code'] < 300) { + return $result; + } elseif (isset($result['message']['message'])) { + throw new Exception($result['message']['message']); + } elseif (isset($result['errors'])) { + $errors = $result['errors'][array_key_first($result['errors'])]; + throw new Exception($errors[0]); + } else { + if (!empty($response['body'])) $this->log('Response:' . $response['body']); + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/goedge.php b/app/lib/deploy/goedge.php new file mode 100644 index 0000000..d6e566f --- /dev/null +++ b/app/lib/deploy/goedge.php @@ -0,0 +1,135 @@ +url = rtrim($config['url'], '/'); + $this->accessKeyId = $config['accessKeyId']; + $this->accessKey = $config['accessKey']; + $this->usertype = $config['usertype']; + $this->systype = $config['systype']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->accessKeyId) || empty($this->accessKey)) throw new Exception('必填参数不能为空'); + $this->getAccessToken(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domains = $config['domainList']; + if (empty($domains)) throw new Exception('没有设置要部署的域名'); + + $this->getAccessToken(); + + $params = [ + 'domains' => $domains, + 'offset' => 0, + 'size' => 10, + ]; + try { + $data = $this->request('/SSLCertService/listSSLCerts', $params); + } catch (Exception $e) { + throw new Exception('获取证书列表失败:' . $e->getMessage()); + } + $list = json_decode(base64_decode($data['sslCertsJSON']), true); + if (!$list || empty($list)) { + throw new Exception('证书列表为空'); + } + $this->log('获取证书列表成功(total=' . count($list) . ')'); + + $certInfo = openssl_x509_parse($fullchain, true); + + foreach ($list as $row) { + $params = [ + 'sslCertId' => $row['id'], + 'isOn' => true, + 'name' => $row['name'], + 'description' => $row['description'], + 'serverName' => $row['serverName'], + 'isCA' => false, + 'certData' => base64_encode($fullchain), + 'keyData' => base64_encode($privatekey), + 'timeBeginAt' => $certInfo['validFrom_time_t'], + 'timeEndAt' => $certInfo['validTo_time_t'], + 'dnsNames' => $domains, + 'commonNames' => [$certInfo['issuer']['CN']], + ]; + $this->request('/SSLCertService/updateSSLCert', $params); + $this->log('证书ID:' . $row['id'] . '更新成功!'); + } + } + + private function getAccessToken() + { + $path = '/APIAccessTokenService/getAPIAccessToken'; + $params = [ + 'type' => $this->usertype, + 'accessKeyId' => $this->accessKeyId, + 'accessKey' => $this->accessKey, + ]; + $result = $this->request($path, $params); + if (isset($result['token'])) { + $this->accessToken = $result['token']; + } else { + throw new Exception('登录成功,获取AccessToken失败'); + } + } + + private function request($path, $params = null) + { + $url = $this->url . $path; + $headers = []; + $body = null; + if ($this->accessToken) { + if ($this->systype == '1') { + $headers[] = 'X-Cloud-Access-Token: ' . $this->accessToken; + } else { + $headers[] = 'X-Edge-Access-Token: ' . $this->accessToken; + } + } + if ($params) { + $headers[] = 'Content-Type: application/json'; + $body = json_encode($params); + } + $response = curl_client($url, $body, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 200) { + return isset($result['data']) ? $result['data'] : null; + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + if (!empty($response['body'])) $this->log('Response:' . $response['body']); + throw new Exception('返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/huawei.php b/app/lib/deploy/huawei.php new file mode 100644 index 0000000..a53d5b4 --- /dev/null +++ b/app/lib/deploy/huawei.php @@ -0,0 +1,147 @@ +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, 'scm.cn-north-4.myhuaweicloud.com'); + $client->request('GET', '/v3/scm/certificates'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + if($config['product'] == 'cdn'){ + $this->deploy_cdn($fullchain, $privatekey, $config); + }elseif($config['product'] == 'elb'){ + $this->deploy_elb($fullchain, $privatekey, $config); + }elseif($config['product'] == 'waf'){ + $this->deploy_waf($fullchain, $privatekey, $config); + } + } + + private function deploy_cdn($fullchain, $privatekey, $config) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, 'cdn.myhuaweicloud.com'); + $param = [ + 'configs' => [ + 'https' => [ + 'https_status' => 'on', + 'certificate_type' => 'server', + 'certificate_source' => 0, + 'certificate_name' => $config['cert_name'], + 'certificate_value' => $fullchain, + 'private_key' => $privatekey, + ], + ], + ]; + $client->request('PUT', '/v1.1/cdn/configuration/domains/'.$config['domain'].'/configs', null, $param); + $this->log('CDN域名 ' . $config['domain'] . ' 部署证书成功!'); + } + + private function deploy_elb($fullchain, $privatekey, $config) + { + if (empty($config['project_id'])) throw new Exception('项目ID不能为空'); + if (empty($config['region_id'])) throw new Exception('区域ID不能为空'); + if (empty($config['cert_id'])) throw new Exception('证书ID不能为空'); + $endpoint = 'elb.'.$config['region_id'].'.myhuaweicloud.com'; + $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, $endpoint); + try{ + $data = $client->request('GET', '/v3/'.$config['project_id'].'/elb/certificates/'.$config['cert_id']); + }catch(Exception $e){ + throw new Exception('证书详情查询失败:'.$e->getMessage()); + } + if(isset($data['certificate']['certificate']) && trim($data['certificate']['certificate']) == trim($fullchain)){ + $this->log('ELB证书ID '.$config['cert_id'].' 已存在,无需重复部署'); + return; + } + $param = [ + 'certificate' => [ + 'certificate' => $fullchain, + 'private_key' => $privatekey, + 'domain' => implode(',', $config['domainList']), + ], + ]; + $client->request('PUT', '/v3/'.$config['project_id'].'/elb/certificates/'.$config['cert_id'], null, $param); + $this->log('ELB证书ID ' . $config['cert_id'] . ' 更新证书成功!'); + } + + private function deploy_waf($fullchain, $privatekey, $config) + { + if (empty($config['project_id'])) throw new Exception('项目ID不能为空'); + if (empty($config['region_id'])) throw new Exception('区域ID不能为空'); + if (empty($config['cert_id'])) throw new Exception('证书ID不能为空'); + $endpoint = 'waf.'.$config['region_id'].'.myhuaweicloud.com'; + $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, $endpoint); + try{ + $data = $client->request('GET', '/v1/'.$config['project_id'].'/waf/certificates/'.$config['cert_id']); + }catch(Exception $e){ + throw new Exception('证书详情查询失败:'.$e->getMessage()); + } + if(isset($data['content']) && trim($data['content']) == trim($fullchain)){ + $this->log('WAF证书ID '.$config['cert_id'].' 已存在,无需重复部署'); + return; + } + $param = [ + 'name' => $config['cert_name'], + 'content' => $fullchain, + 'key' => $privatekey, + ]; + $client->request('PUT', '/v1/'.$config['project_id'].'/waf/certificates/'.$config['cert_id'], null, $param); + $this->log('WAF证书ID ' . $config['cert_id'] . ' 更新证书成功!'); + } + + private function get_cert_id($fullchain, $privatekey) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, 'scm.cn-north-4.myhuaweicloud.com'); + $param = [ + 'name' => $cert_name, + 'certificate' => $fullchain, + 'private_key' => $privatekey, + ]; + try { + $data = $client->request('POST', '/v3/scm/certificates/import', null, $param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $this->log('上传证书成功 certificate_id=' . $data['certificate_id']); + return $data['certificate_id']; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/huoshan.php b/app/lib/deploy/huoshan.php new file mode 100644 index 0000000..b663758 --- /dev/null +++ b/app/lib/deploy/huoshan.php @@ -0,0 +1,96 @@ +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'cdn.volcengineapi.com', 'cdn', '2021-03-01', 'cn-north-1'); + $client->request('POST', 'ListCertInfo', ['Source' => 'volc_cert_center']); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $cert_id = $this->get_cert_id($fullchain, $privatekey); + if (!$cert_id) throw new Exception('获取证书ID失败'); + $info['cert_id'] = $cert_id; + $this->deploy_cdn($cert_id, $config); + } + + private function deploy_cdn($cert_id, $config) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'cdn.volcengineapi.com', 'cdn', '2021-03-01', 'cn-north-1'); + $param = [ + 'CertId' => $cert_id, + 'Domain' => $config['domain'], + ]; + $data = $client->request('POST', 'BatchDeployCert', $param); + if (empty($data['DeployResult'])) throw new Exception('部署证书失败:DeployResult为空'); + foreach ($data['DeployResult'] as $row) { + if ($row['Status'] == 'success') { + $this->log('CDN域名 ' . $row['Domain'] . ' 部署证书成功!'); + } else { + $this->log('CDN域名 ' . $row['Domain'] . ' 部署证书失败:' . isset($row['ErrorMsg']) ? $row['ErrorMsg'] : ''); + } + } + } + + private function get_cert_id($fullchain, $privatekey) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'cdn.volcengineapi.com', 'cdn', '2021-03-01', 'cn-north-1'); + $param = [ + 'Source' => 'volc_cert_center', + 'Certificate' => $fullchain, + 'PrivateKey' => $privatekey, + 'Desc' => $cert_name, + 'Repeatable' => false, + ]; + try { + $data = $client->request('POST', 'AddCertificate', $param); + } catch (Exception $e) { + if(strpos($e->getMessage(), '证书已存在,ID为')!==false){ + $cert_id = trim(getSubstr($e->getMessage(), '证书已存在,ID为', '。')); + $this->log('证书已存在 CertId=' . $cert_id); + return $cert_id; + } + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $this->log('上传证书成功 CertId=' . $data['CertId']); + return $data['CertId']; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/kangle.php b/app/lib/deploy/kangle.php new file mode 100644 index 0000000..c8e15a8 --- /dev/null +++ b/app/lib/deploy/kangle.php @@ -0,0 +1,208 @@ +url = rtrim($config['url'], '/'); + $this->auth = $config['auth']; + $this->username = $config['username']; + $this->password = $config['password']; + $this->skey = $config['skey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->username)) throw new Exception('必填参数不能为空'); + $this->login(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $this->login(); + $this->log('登录成功 cookie:' . $this->cookie); + $this->getMain(); + + if ($config['type'] == '1' && !empty($config['domains'])) { + $domains = explode("\n", $config['domains']); + $success = 0; + $errmsg = null; + foreach ($domains as $domain) { + $domain = trim($domain); + if (empty($domain)) continue; + try { + $this->deployDomain($domain, $fullchain, $privatekey); + $this->log("域名 {$domain} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("域名 {$domain} 证书部署失败:" . $errmsg); + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '要部署的域名不存在'); + } + } else { + $this->deployAccount($fullchain, $privatekey); + $this->log("账号级SSL证书部署成功"); + } + } + + private function deployDomain($domain, $fullchain, $privatekey) + { + $path = '/vhost/?c=ssl&a=domainSsl'; + $post = [ + 'domain' => $domain, + 'certificate' => $fullchain, + 'certificate_key' => $privatekey, + ]; + $response = curl_client($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); + if (strpos($response['body'], '成功')) { + return true; + } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { + throw new Exception(htmlspecialchars($match[1])); + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception(htmlspecialchars($response['body'])); + } else { + throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); + } + } + + private function deployAccount($fullchain, $privatekey) + { + $path = '/vhost/?c=ssl&a=ssl'; + $post = [ + 'certificate' => $fullchain, + 'certificate_key' => $privatekey, + ]; + $response = curl_client($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); + if (strpos($response['body'], '成功')) { + return true; + } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { + throw new Exception(htmlspecialchars($match[1])); + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception(htmlspecialchars($response['body'])); + } else { + throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); + } + } + + private function login() + { + if ($this->auth == '1') { + return $this->loginBySkey(); + } else { + return $this->loginByPwd(); + } + } + + private function loginBySkey() + { + $url = $this->url . '/vhost/index.php?c=sso&a=hello&url=' . urlencode($this->url . '/index.php?'); + $response = curl_client($url, null, null, null, null, $this->proxy); + if ($response['code'] == 302 && !empty($response['redirect_url'])) { + $cookie = ''; + if (preg_match_all('/Set-Cookie: (.*);/iU', $response['header'], $matchs)) { + foreach ($matchs[1] as $val) { + $arr = explode('=', $val); + if ($arr[1] == '' || $arr[1] == 'deleted') continue; + $cookie .= $val . '; '; + } + $query = parse_url($response['redirect_url'], PHP_URL_QUERY); + parse_str($query, $params); + if (isset($params['r'])) { + $sess_key = $params['r']; + $this->loginBySkey2($cookie, $sess_key); + $this->cookie = $cookie; + return true; + } else { + throw new Exception('获取SSO凭据失败,sess_key获取失败'); + } + } else { + throw new Exception('获取SSO凭据失败,获取cookie失败'); + } + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception('获取SSO凭据失败 (' . htmlspecialchars($response['body']) . ')'); + } else { + throw new Exception('获取SSO凭据失败 (httpCode=' . $response['code'] . ')'); + } + } + + private function loginBySkey2($cookie, $sess_key) + { + $s = md5($sess_key . $this->username . $sess_key . $this->skey); + $url = $this->url . '/vhost/index.php?c=sso&a=login&name=' . $this->username . '&r=' . $sess_key . '&s=' . $s; + $response = curl_client($url, null, null, $cookie, null, $this->proxy); + if ($response['code'] == 302) { + return true; + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception('SSO登录失败 (' . htmlspecialchars($response['body']) . ')'); + } else { + throw new Exception('SSO登录失败 (httpCode=' . $response['code'] . ')'); + } + } + + private function loginByPwd() + { + $referer = $this->url . '/vhost/index.php?c=session&a=loginForm'; + $url = $this->url . '/vhost/index.php?c=session&a=login'; + $post = [ + 'username' => $this->username, + 'passwd' => $this->password, + ]; + $response = curl_client($url, http_build_query($post), $referer, null, null, $this->proxy); + if ($response['code'] == 302) { + $cookie = ''; + if (preg_match_all('/Set-Cookie: (.*);/iU', $response['header'], $matchs)) { + foreach ($matchs[1] as $val) { + $arr = explode('=', $val); + if ($arr[1] == '' || $arr[1] == 'deleted') continue; + $cookie .= $val . '; '; + } + $this->cookie = $cookie; + return true; + } else { + throw new Exception('登录失败,获取cookie失败'); + } + } elseif (strpos($response['body'], '验证码错误')) { + throw new Exception('登录失败,需输入验证码'); + } elseif (strpos($response['body'], '密码错误')) { + throw new Exception('登录失败,用户名或密码错误'); + } else { + throw new Exception('登录失败 (httpCode=' . $response['code'] . ')'); + } + } + + private function getMain() + { + $path = '/vhost/'; + curl_client($this->url . $path, null, null, $this->cookie, null, $this->proxy); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/lecdn.php b/app/lib/deploy/lecdn.php new file mode 100644 index 0000000..2e64c99 --- /dev/null +++ b/app/lib/deploy/lecdn.php @@ -0,0 +1,106 @@ +url = rtrim($config['url'], '/'); + $this->email = $config['email']; + $this->password = $config['password']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->email) || empty($this->password)) throw new Exception('账号和密码不能为空'); + $this->login(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $id = $config['id']; + if (empty($id)) throw new Exception('证书ID不能为空'); + + $this->login(); + + try { + $data = $this->request('/prod-api/certificate/' . $id); + } catch (Exception $e) { + throw new Exception('证书ID:' . $id . '获取失败:' . $e->getMessage()); + } + + $params = [ + 'id' => intval($id), + 'name' => $data['name'], + 'description' => $data['description'], + 'type' => 'upload', + 'ssl_pem' => base64_encode($fullchain), + 'ssl_key' => base64_encode($privatekey), + 'auto_renewal' => false, + ]; + $this->request('/prod-api/certificate/' . $id, $params, 'PUT'); + $this->log("证书ID:{$id}更新成功!"); + } + + private function login() + { + $path = '/prod-api/login'; + $params = [ + 'email' => $this->email, + 'password' => $this->password, + ]; + $result = $this->request($path, $params); + if (isset($result['access_token'])) { + $this->accessToken = $result['access_token']; + } else { + throw new Exception('登录成功,获取access_token失败'); + } + } + + private function request($path, $params = null, $method = null) + { + $url = $this->url . $path; + $headers = []; + $body = null; + if ($this->accessToken) { + $headers[] = 'Authorization: Bearer ' . $this->accessToken; + } + if ($params) { + $headers[] = 'Content-Type: application/json;charset=UTF-8'; + $body = json_encode($params); + } + $response = curl_client($url, $body, null, null, $headers, $this->proxy, $method); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 0) { + return isset($result['data']) ? $result['data'] : null; + } elseif (isset($result['message'])) { + throw new Exception($result['message']); + } else { + throw new Exception('返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/local.php b/app/lib/deploy/local.php new file mode 100644 index 0000000..71b7027 --- /dev/null +++ b/app/lib/deploy/local.php @@ -0,0 +1,77 @@ +log('证书已保存到:' . $config['pem_cert_file']); + } else { + throw new Exception('证书保存到' . $config['pem_cert_file'] . '失败,请检查目录权限'); + } + if (file_put_contents($config['pem_key_file'], $privatekey)) { + $this->log('私钥已保存到:' . $config['pem_key_file']); + } else { + throw new Exception('私钥保存到' . $config['pem_key_file'] . '失败,请检查目录权限'); + } + } elseif ($config['format'] == 'pfx') { + $dir = dirname($config['pfx_file']); + if (!is_dir($dir)) throw new Exception($dir.' 目录不存在'); + if (!is_writable($dir)) throw new Exception($dir.' 目录不可写'); + + $pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null); + if (file_put_contents($config['pfx_file'], $pfx)) { + $this->log('PFX证书已保存到:' . $config['pfx_file']); + } else { + throw new Exception('PFX证书保存到' . $config['pfx_file'] . '失败,请检查目录权限'); + } + } + if (!empty($config['cmd'])) { + $cmds = explode("\n", $config['cmd']); + foreach($cmds as $cmd){ + $cmd = trim($cmd); + if(empty($cmd)) continue; + $this->log('执行命令:'.$cmd); + $output = []; + $ret = 0; + exec($cmd, $output, $ret); + if ($ret == 0) { + $this->log('执行命令成功:' . implode("\n", $output)); + } else { + throw new Exception('执行命令失败:' . implode("\n", $output)); + } + } + } + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + public function setLogger($logger) + { + $this->logger = $logger; + } +} diff --git a/app/lib/deploy/opanel.php b/app/lib/deploy/opanel.php new file mode 100644 index 0000000..31dca1a --- /dev/null +++ b/app/lib/deploy/opanel.php @@ -0,0 +1,111 @@ +url = rtrim($config['url'], '/'); + $this->key = $config['key']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥'); + $this->request('/api/v1/settings/search'); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domains = $config['domainList']; + if (empty($domains)) throw new Exception('没有设置要部署的域名'); + + $params = ['page'=>1, 'pageSize'=>500]; + try { + $data = $this->request('/api/v1/websites/ssl/search', $params); + $this->log('获取证书列表成功(total=' . $data['total'] . ')'); + } catch (Exception $e) { + throw new Exception('获取证书列表失败:' . $e->getMessage()); + } + + $success = 0; + $errmsg = null; + foreach ($data['items'] as $row) { + if (empty($row['primaryDomain'])) continue; + $cert_domains[] = $row['primaryDomain']; + if(!empty($row['domains'])) $cert_domains += explode(',', $row['domains']); + $flag = false; + foreach ($cert_domains as $domain) { + if (in_array($domain, $domains)) { + $flag = true; + break; + } + } + if ($flag) { + $params = [ + 'sslID' => $row['id'], + 'type' => 'paste', + 'certificate' => $fullchain, + 'privateKey' => $privatekey, + 'description' => '', + ]; + try { + $this->request('/api/v1/websites/ssl/upload', $params); + $this->log("证书ID:{$row['id']}更新成功!"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("证书ID:{$row['id']}更新失败:" . $errmsg); + } + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '没有要更新的证书'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($path, $params = null) + { + $url = $this->url . $path; + + $timestamp = getMillisecond(); + $token = md5('1panel' . $this->key . $timestamp); + $headers = [ + '1Panel-Token: '.$token, + '1Panel-Timestamp: '.$timestamp + ]; + $body = $params ? json_encode($params) : null; + if($body) $headers[] = 'Content-Type: application/json'; + $response = curl_client($url, $body, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if(isset($result['code']) && $result['code'] == 200){ + return isset($result['data']) ? $result['data'] : null; + }elseif(isset($result['message'])){ + throw new Exception($result['message']); + }else{ + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } +} diff --git a/app/lib/deploy/qiniu.php b/app/lib/deploy/qiniu.php new file mode 100644 index 0000000..1bc2c07 --- /dev/null +++ b/app/lib/deploy/qiniu.php @@ -0,0 +1,145 @@ +AccessKey = $config['AccessKey']; + $this->SecretKey = $config['SecretKey']; + $this->client = new QiniuClient($this->AccessKey, $this->SecretKey); + } + + public function check() + { + if (empty($this->AccessKey) || empty($this->SecretKey)) throw new Exception('必填参数不能为空'); + $this->client->request('GET', '/sslcert'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domain = $config['domain']; + if (empty($domain)) throw new Exception('绑定的域名不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $cert_id = $this->get_cert_id($fullchain, $privatekey, $certInfo['subject']['CN'], $cert_name); + + if($config['product'] == 'cdn'){ + $this->deploy_cdn($domain, $cert_id); + }elseif($config['product'] == 'oss'){ + $this->deploy_oss($domain, $cert_id); + }else{ + throw new Exception('未知的产品类型'); + } + $info['cert_id'] = $cert_id; + $info['cert_name'] = $cert_name; + } + + private function deploy_cdn($domain, $cert_id) + { + try { + $data = $this->client->request('GET', '/domain/' . $domain); + } catch (Exception $e) { + throw new Exception('获取域名信息失败:' . $e->getMessage()); + } + if (isset($data['https']['certId']) && $data['https']['certId'] == $cert_id) { + $this->log('域名 ' . $domain . ' 证书已部署,无需重复操作'); + return; + } + + if (empty($data['https']['certId'])) { + $param = [ + 'certid' => $cert_id, + ]; + $this->client->request('PUT', '/domain/' . $domain . '/sslize', null, $param); + } else { + $param = [ + 'certid' => $cert_id, + 'forceHttps' => $data['https']['forceHttps'], + 'http2Enable' => $data['https']['http2Enable'], + ]; + $this->client->request('PUT', '/domain/' . $domain . '/httpsconf', null, $param); + } + $this->log('CDN域名 ' . $domain . ' 证书部署成功!'); + } + + private function deploy_oss($domain, $cert_id) + { + $param = [ + 'certid' => $cert_id, + 'domain' => $domain, + ]; + $this->client->request('POST', '/cert/bind', null, $param); + $this->log('OSS域名 ' . $domain . ' 证书部署成功!'); + } + + private function get_cert_id($fullchain, $privatekey, $common_name, $cert_name) + { + $cert_id = null; + $marker = ''; + do { + $query = ['marker' => $marker, 'limit' => 100]; + $data = $this->client->request('GET', '/sslcert', $query); + if (empty($data['certs'])) break; + $marker = $data['marker']; + foreach ($data['certs'] as $cert) { + if ($cert_name == $cert['name']) { + $cert_id = $cert['certid']; + $this->log('证书' . $cert_name . '已存在,证书ID:' . $cert_id); + } elseif ($cert['not_after'] < time()) { + try { + $this->client->request('DELETE', '/sslcert/' . $cert['certid']); + $this->log('证书' . $cert['name'] . '已过期,删除证书成功'); + } catch (Exception $e) { + $this->log('证书' . $cert['name'] . '已过期,删除证书失败:' . $e->getMessage()); + } + usleep(300000); + } + } + } while ($marker != ''); + + if (!$cert_id) { + $param = [ + 'name' => $cert_name, + 'common_name' => $common_name, + 'pri' => $privatekey, + 'ca' => $fullchain, + ]; + try { + $data = $this->client->request('POST', '/sslcert', null, $param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $this->log('上传证书成功,证书ID:' . $data['certID']); + $cert_id = $data['certID']; + usleep(500000); + } + return $cert_id; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/safeline.php b/app/lib/deploy/safeline.php new file mode 100644 index 0000000..66d9a46 --- /dev/null +++ b/app/lib/deploy/safeline.php @@ -0,0 +1,104 @@ +url = rtrim($config['url'], '/'); + $this->token = $config['token']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->token)) throw new Exception('请填写控制台地址和API Token'); + $this->request('/api/open/system'); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domains = $config['domainList']; + if (empty($domains)) throw new Exception('没有设置要部署的域名'); + + try { + $data = $this->request('/api/open/cert'); + $this->log('获取证书列表成功(total=' . $data['total'] . ')'); + } catch (Exception $e) { + throw new Exception('获取证书列表失败:' . $e->getMessage()); + } + + $success = 0; + $errmsg = null; + foreach ($data['nodes'] as $row) { + if (empty($row['domains'])) continue; + $flag = false; + foreach ($row['domains'] as $domain) { + if (in_array($domain, $domains)) { + $flag = true; + break; + } + } + if ($flag) { + $params = [ + 'id' => $row['id'], + 'manual' => [ + 'crt' => $fullchain, + 'key' => $privatekey, + ], + 'type' => 2, + ]; + try { + $this->request('/api/open/cert', $params); + $this->log("证书ID:{$row['id']}更新成功!"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("证书ID:{$row['id']}更新失败:" . $errmsg); + } + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '没有要更新的证书'); + } + } + + private function request($path, $params = null) + { + $url = $this->url . $path; + $headers = ['X-SLCE-API-TOKEN: ' . $this->token]; + $body = null; + if ($params) { + $heders[] = 'Content-Type: application/json'; + $body = json_encode($params); + } + $response = curl_client($url, $body, null, null, $headers, $this->proxy); + $result = json_decode($response['body'], true); + if ($response['code'] == 200 && $result) { + return isset($result['data']) ? $result['data'] : null; + } else { + throw new Exception(!empty($result['msg']) ? $result['msg'] : '请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/ssh.php b/app/lib/deploy/ssh.php new file mode 100644 index 0000000..3ac10f1 --- /dev/null +++ b/app/lib/deploy/ssh.php @@ -0,0 +1,205 @@ +config = $config; + } + + public function check() + { + $this->connect(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $connection = $this->connect(); + $sftp = ssh2_sftp($connection); + if ($config['format'] == 'pem') { + $stream = fopen("ssh2.sftp://$sftp{$config['pem_cert_file']}", 'w'); + if (!$stream) { + throw new Exception("无法创建证书文件:{$config['pem_cert_file']}"); + } + fwrite($stream, $fullchain); + fclose($stream); + $this->log('证书已保存到:' . $config['pem_cert_file']); + + $stream = fopen("ssh2.sftp://$sftp{$config['pem_key_file']}", 'w'); + if (!$stream) { + throw new Exception("无法创建私钥文件:{$config['pem_key_file']}"); + } + fwrite($stream, $privatekey); + fclose($stream); + $this->log('私钥已保存到:' . $config['pem_key_file']); + } elseif ($config['format'] == 'pfx') { + $pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null); + + $stream = fopen("ssh2.sftp://$sftp{$config['pfx_file']}", 'w'); + if (!$stream) { + throw new Exception("无法创建PFX证书文件:{$config['pfx_file']}"); + } + fwrite($stream, $pfx); + fclose($stream); + $this->log('PFX证书已保存到:' . $config['pfx_file']); + + if ($config['uptype'] == '1' && !empty($config['iis_domain'])) { + $cert_hash = openssl_x509_fingerprint($fullchain, 'sha1'); + $this->deploy_iis($connection, $config['iis_domain'], $config['pfx_file'], $config['pfx_pass'], $cert_hash); + $config['cmd'] = null; + } + } + if (!empty($config['cmd'])) { + $cmds = explode("\n", $config['cmd']); + foreach ($cmds as $cmd) { + $cmd = trim($cmd); + if (empty($cmd)) continue; + $this->exec($connection, $cmd); + } + } + } + + private function deploy_iis($connection, $domain, $pfx_file, $pfx_pass, $cert_hash) + { + if (!strpos($domain, ':')) { + $domain .= ':443'; + } + $ret = $this->exec($connection, 'netsh http show sslcert hostnameport=' . $domain); + if (preg_match('/:\s+(\w{40})/', $ret, $match)) { + if ($match[1] == $cert_hash) { + $this->log('IIS域名 ' . $domain . ' 证书已存在,无需更新'); + return; + } + } + $p = '-p ""'; + if (!empty($pfx_pass)) $p = '-p ' . $pfx_pass; + if (substr($pfx_file, 0, 1) == '/') $pfx_file = substr($pfx_file, 1); + $this->exec($connection, 'certutil ' . $p . ' -importPFX ' . $pfx_file); + $this->exec($connection, 'netsh http delete sslcert hostnameport=' . $domain); + $this->exec($connection, 'netsh http add sslcert hostnameport=' . $domain . ' certhash=' . $cert_hash . ' certstorename=MY appid=\'{' . $this->uuid() . '}\''); + $this->log('IIS域名 ' . $domain . ' 证书已更新'); + } + + private function uuid() + { + $guid = md5(uniqid(mt_rand(), true)); + return substr($guid, 0, 8) . '-' . substr($guid, 8, 4) . '-4' . substr($guid, 12, 3) . '-' . substr($guid, 16, 4) . '-' . substr($guid, 20, 12); + } + + private function exec($connection, $cmd) + { + $this->log('执行命令:' . $cmd); + $stream = ssh2_exec($connection, $cmd); + $errorStream = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR); + if (!$stream || !$errorStream) { + throw new Exception('执行命令失败'); + } + stream_set_blocking($stream, true); + stream_set_blocking($errorStream, true); + $output = stream_get_contents($stream); + $errorOutput = stream_get_contents($errorStream); + fclose($stream); + fclose($errorStream); + if (trim($errorOutput)) { + if ($this->config['windows'] == '1' && $this->containsGBKChinese($errorOutput)) { + $errorOutput = mb_convert_encoding($errorOutput, 'UTF-8', 'GBK'); + } + throw new Exception('执行命令失败:' . trim($errorOutput)); + } else { + if ($this->config['windows'] == '1' && $this->containsGBKChinese($output)) { + $output = mb_convert_encoding($output, 'UTF-8', 'GBK'); + } + $this->log('执行命令成功:' . trim($output)); + return $output; + } + } + + private function connect() + { + if (!function_exists('ssh2_connect')) { + throw new Exception('ssh2扩展未安装'); + } + if (empty($this->config['host']) || empty($this->config['port']) || empty($this->config['username']) || $this->config['auth'] == '0' && empty($this->config['password']) || $this->config['auth'] == '1' && empty($this->config['privatekey'])) { + throw new Exception('必填参数不能为空'); + } + if (!filter_var($this->config['host'], FILTER_VALIDATE_IP) && !filter_var($this->config['host'], FILTER_VALIDATE_DOMAIN)) { + throw new Exception('主机地址不合法'); + } + if (!is_numeric($this->config['port']) || $this->config['port'] < 1 || $this->config['port'] > 65535) { + throw new Exception('端口不合法'); + } + + $connection = ssh2_connect($this->config['host'], intval($this->config['port'])); + if (!$connection) { + throw new Exception('SSH连接失败'); + } + if ($this->config['auth'] == '1') { + $publicKey = $this->getPublicKey($this->config['privatekey']); + $publicKeyPath = app()->getRuntimePath() . $this->config['host'] . '.pub'; + $privateKeyPath = app()->getRuntimePath() . $this->config['host'] . '.key'; + $umask = umask(0066); + file_put_contents($privateKeyPath, $this->config['privatekey']); + file_put_contents($publicKeyPath, $publicKey); + umask($umask); + if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) { + throw new Exception('私钥认证失败'); + } + } else { + if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) { + throw new Exception('用户名或密码错误'); + } + } + return $connection; + } + + private function getPublicKey($privateKey) + { + $res = openssl_pkey_get_private($privateKey); + if (!$res) { + throw new Exception('加载私钥失败'); + } + $details = openssl_pkey_get_details($res); + if (!$details || !isset($details['key'])) { + throw new Exception('从私钥导出公钥失败'); + } + $buffer = pack("N", 7) . "ssh-rsa" . + $this->sshEncodeBuffer($details['rsa']['e']) . + $this->sshEncodeBuffer($details['rsa']['n']); + return "ssh-rsa " . base64_encode($buffer); + } + + private function sshEncodeBuffer($buffer) + { + $len = strlen($buffer); + if (ord($buffer[0]) & 0x80) { + $len++; + $buffer = "\x00" . $buffer; + } + return pack("Na*", $len, $buffer); + } + + private function containsGBKChinese($string) + { + return preg_match('/[\x81-\xFE][\x40-\xFE]/', $string) === 1; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + public function setLogger($logger) + { + $this->logger = $logger; + } +} diff --git a/app/lib/deploy/tencent.php b/app/lib/deploy/tencent.php new file mode 100644 index 0000000..1cde46a --- /dev/null +++ b/app/lib/deploy/tencent.php @@ -0,0 +1,249 @@ +SecretId = $config['SecretId']; + $this->SecretKey = $config['SecretKey']; + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05'); + } + + public function check() + { + if (empty($this->SecretId) || empty($this->SecretKey)) throw new Exception('必填参数不能为空'); + $this->client->request('DescribeCertificates', []); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $cert_id = $this->get_cert_id($fullchain, $privatekey); + if (!$cert_id) throw new Exception('证书ID获取失败'); + if ($config['product'] == 'cos') { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + if (empty($config['cos_bucket'])) throw new Exception('存储桶名称不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $instance_id = $config['regionid'] . '#' . $config['cos_bucket'] . '#' . $config['domain']; + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid']); + } elseif ($config['product'] == 'tke') { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + if (empty($config['tke_cluster_id'])) throw new Exception('集群ID不能为空'); + if (empty($config['tke_namespace'])) throw new Exception('命名空间不能为空'); + if (empty($config['tke_secret'])) throw new Exception('secret名称不能为空'); + $instance_id = $config['tke_cluster_id'] . '|' . $config['tke_namespace'] . '|' . $config['tke_secret']; + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid']); + } elseif ($config['product'] == 'lighthouse') { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + if (empty($config['lighthouse_id'])) throw new Exception('实例ID不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $instance_id = $config['regionid'] . '|' . $config['lighthouse_id'] . '|' . $config['domain']; + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid']); + } elseif ($config['product'] == 'clb') { + return $this->deploy_clb($cert_id, $config); + } elseif ($config['product'] == 'scf') { + return $this->deploy_scf($cert_id, $config); + } else { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + if ($config['product'] == 'waf') { + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['region']); + } elseif (in_array($config['product'], ['tse','scf'])) { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid']); + } + $instance_id = $config['domain']; + } + try { + $record_id = $this->deploy_common($config['product'], $cert_id, $instance_id); + $info['cert_id'] = $cert_id; + $info['record_id'] = $record_id; + } catch (Exception $e) { + if (isset($info['record_id'])) { + if ($this->deploy_query($info['record_id'])) { + $this->log(strtoupper($config['product']) . '实例 ' . $instance_id . ' 已部署证书,无需重复部署'); + return; + } + } + throw $e; + } + } + + private function get_cert_id($fullchain, $privatekey) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $param = [ + 'CertificatePublicKey' => $fullchain, + 'CertificatePrivateKey' => $privatekey, + 'CertificateType' => 'SVR', + 'Alias' => $cert_name, + 'Repeatable' => false, + ]; + try { + $data = $this->client->request('UploadCertificate', $param); + } catch (Exception $e) { + throw new Exception('上传证书失败:' . $e->getMessage()); + } + $this->log('上传证书成功 CertificateId=' . $data['CertificateId']); + usleep(300000); + return $data['CertificateId']; + } + + private function deploy_common($product, $cert_id, $instance_id) + { + $param = [ + 'CertificateId' => $cert_id, + 'InstanceIdList' => [$instance_id], + 'ResourceType' => $product, + ]; + $data = $this->client->request('DeployCertificateInstance', $param); + $this->log(json_encode($data)); + $this->log(strtoupper($product) . '实例 ' . $instance_id . ' 部署证书成功!'); + return $data['DeployRecordId']; + } + + private function deploy_query($record_id) + { + $param = [ + 'DeployRecordId' => strval($record_id), + ]; + try { + $data = $this->client->request('DescribeHostDeployRecordDetail', $param); + if (isset($data['SuccessTotalCount']) && $data['SuccessTotalCount'] >= 1 || isset($data['RunningTotalCount']) && $data['RunningTotalCount'] >= 1) { + return true; + } + if (isset($data['FailedTotalCount']) && $data['FailedTotalCount'] >= 1 && !empty($data['DeployRecordDetailList'])) { + $errmsg = $data['DeployRecordDetailList'][0]['ErrorMsg']; + if (strpos($errmsg, '\u')) { + $errmsg = json_decode($errmsg); + } + $this->log('证书部署失败原因:' . $errmsg); + } + } catch (Exception $e) { + $this->log('查询证书部署记录失败:' . $e->getMessage()); + } + return false; + } + + private function deploy_clb($cert_id, $config) + { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + if (empty($config['clb_id'])) throw new Exception('负载均衡ID不能为空'); + $sni_switch = !empty($config['clb_domain']) ? 1 : 0; + + $client = new TencentCloud($this->SecretId, $this->SecretKey, 'clb.tencentcloudapi.com', 'clb', '2018-03-17', $config['regionid']); + $param = [ + 'LoadBalancerId' => $config['clb_id'], + 'Protocol' => 'HTTPS', + ]; + if (!empty($config['clb_listener_id'])) { + $param['ListenerIds'] = [$config['clb_listener_id']]; + } + try { + $data = $client->request('DescribeListeners', $param); + } catch (Exception $e) { + throw new Exception('获取监听器列表失败:' . $e->getMessage()); + } + if (!isset($data['TotalCount']) || $data['TotalCount'] == 0) throw new Exception('负载均衡:' . $config['clb_id'] . '监听器列表为空'); + $count = 0; + foreach ($data['Listeners'] as $listener) { + if ($listener['SniSwitch'] == $sni_switch) { + if ($sni_switch == 1) { + foreach ($listener['Rules'] as $rule) { + if ($rule['Domain'] == $config['clb_domain']) { + if (isset($rule['Certificate']['CertId']) && $cert_id == $rule['Certificate']['CertId']) { + $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 域名 ' . $rule['Domain'] . ' 已部署证书,无需重复部署'); + } else { + $param = [ + 'LoadBalancerId' => $config['clb_id'], + 'ListenerId' => $listener['ListenerId'], + 'Domain' => $rule['Domain'], + 'Certificate' => [ + 'SSLMode' => 'UNIDIRECTIONAL', + 'CertId' => $cert_id, + ], + ]; + $client->request('ModifyDomainAttributes', $param); + $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 域名 ' . $rule['Domain'] . ' 部署证书成功!'); + } + $count++; + } + } + } else { + if (isset($listener['Certificate']['CertId']) && $cert_id == $listener['Certificate']['CertId']) { + $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 已部署证书,无需重复部署'); + } else { + $param = [ + 'LoadBalancerId' => $config['clb_id'], + 'ListenerId' => $listener['ListenerId'], + 'Certificate' => [ + 'SSLMode' => 'UNIDIRECTIONAL', + 'CertId' => $cert_id, + ], + ]; + $client->request('ModifyListener', $param); + $this->log('负载均衡监听器 ' . $listener['ListenerId'] . ' 部署证书成功!'); + } + $count++; + } + } + } + if ($count == 0) throw new Exception('没有找到要更新证书的监听器'); + } + + private function deploy_scf($cert_id, $config) + { + if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + + $client = new TencentCloud($this->SecretId, $this->SecretKey, 'scf.tencentcloudapi.com', 'scf', '2018-04-16', $config['regionid']); + $param = [ + 'Domain' => $config['domain'], + ]; + try { + $data = $client->request('GetCustomDomain', $param); + } catch (Exception $e) { + throw new Exception('获取云函数自定义域名失败:' . $e->getMessage()); + } + + if(isset($data['CertConfig']['CertificateId']) && $data['CertConfig']['CertificateId'] == $cert_id){ + $this->log('云函数自定义域名 ' . $config['domain'] . ' 已部署证书,无需重复部署'); + return; + } + $data['CertConfig']['CertificateId'] = $cert_id; + if($data['Protocol'] == 'HTTP') $data['Protocol'] = 'HTTP&HTTPS'; + + $param = [ + 'Domain' => $config['domain'], + 'Protocol' => $data['Protocol'], + 'CertConfig' => $data['CertConfig'], + ]; + $data = $client->request('UpdateCustomDomain', $param); + $this->log('云函数自定义域名 ' . $config['domain'] . ' 部署证书成功!'); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/ucloud.php b/app/lib/deploy/ucloud.php new file mode 100644 index 0000000..f2db275 --- /dev/null +++ b/app/lib/deploy/ucloud.php @@ -0,0 +1,132 @@ +PublicKey = $config['PublicKey']; + $this->PrivateKey = $config['PrivateKey']; + $this->client = new UcloudClient($this->PublicKey, $this->PrivateKey); + } + + public function check() + { + if (empty($this->PublicKey) || empty($this->PrivateKey)) throw new Exception('必填参数不能为空'); + $param = ['Mode' => 'free']; + $this->client->request('GetCertificateList', $param); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $domain_id = $config['domain_id']; + if (empty($domain_id)) throw new Exception('云分发资源ID不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace(['*', '.'], '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + try { + $data = $this->client->request('GetCertificateV2', []); + } catch (Exception $e) { + throw new Exception('获取证书列表失败 ' . $e->getMessage()); + } + + $exist = false; + foreach ($data['CertList'] as $cert) { + if (trim($cert['UserCert']) == trim($fullchain)) { + $cert_name = $cert['CertName']; + $exist = true; + } + } + + if (!$exist) { + $param = [ + 'CertName' => $cert_name, + 'UserCert' => $fullchain, + 'PrivateKey' => $privatekey, + ]; + try { + $data = $this->client->request('AddCertificate', $param); + } catch (Exception $e) { + throw new Exception('添加证书失败 ' . $e->getMessage()); + } + $this->log('添加证书成功,名称:' . $cert_name); + } else { + $this->log('获取到已添加的证书,名称:' . $cert_name); + } + + try { + $data = $this->client->request('GetUcdnDomainConfig', ['DomainId.0' => $domain_id]); + } catch (Exception $e) { + throw new Exception('获取加速域名配置失败 ' . $e->getMessage()); + } + if (empty($data['DomainList'])) throw new Exception('云分发资源ID:' . $domain_id . '不存在'); + $domain = $data['DomainList'][0]['Domain']; + $HttpsStatusCn = $data['DomainList'][0]['HttpsStatusCn']; + $HttpsStatusAbroad = $data['DomainList'][0]['HttpsStatusAbroad']; + + if ($data['DomainList'][0]['CertNameCn'] == $cert_name || $data['DomainList'][0]['CertNameAbroad'] == $cert_name) { + $this->log('云分发' . $domain_id . '证书已配置,无需重复操作'); + return; + } + + try { + $data = $this->client->request('GetCertificateBaseInfoList', ['Domain' => $domain]); + } catch (Exception $e) { + throw new Exception('获取可用证书列表失败 ' . $e->getMessage()); + } + if (empty($data['CertList'])) throw new Exception('可用证书列表为空'); + + $cert_id = null; + foreach ($data['CertList'] as $cert) { + if ($cert['CertName'] == $cert_name) { + $cert_id = $cert['CertId']; + break; + } + } + if (!$cert_id) throw new Exception('证书ID不存在'); + $this->log('证书ID获取成功:' . $cert_id); + + $param = [ + 'DomainId' => $domain_id, + 'CertName' => $cert_name, + 'CertId' => $cert_id, + 'CertType' => 'ucdn', + ]; + if ($HttpsStatusCn == 'enable') $param['HttpsStatusCn'] = $HttpsStatusCn; + if ($HttpsStatusAbroad == 'enable') $param['HttpsStatusAbroad'] = $HttpsStatusAbroad; + if ($HttpsStatusCn != 'enable' && $HttpsStatusAbroad != 'enable') $param['HttpsStatusCn'] = 'enable'; + try { + $data = $this->client->request('UpdateUcdnDomainHttpsConfigV2', $param); + } catch (Exception $e) { + throw new Exception('https加速配置失败 ' . $e->getMessage()); + } + $this->log('云分发' . $domain_id . '证书配置成功!'); + $info['cert_id'] = $cert_id; + $info['cert_name'] = $cert_name; + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/dns/aliyun.php b/app/lib/dns/aliyun.php index f53c3c3..df8cd1f 100644 --- a/app/lib/dns/aliyun.php +++ b/app/lib/dns/aliyun.php @@ -3,6 +3,8 @@ namespace app\lib\dns; use app\lib\DnsInterface; +use app\lib\client\Aliyun as AliyunClient; +use Exception; class aliyun implements DnsInterface { @@ -14,11 +16,13 @@ class aliyun implements DnsInterface private $domain; private $domainid; private $domainInfo; + private AliyunClient $client; public function __construct($config) { $this->AccessKeyId = $config['ak']; $this->AccessKeySecret = $config['sk']; + $this->client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $this->Endpoint, $this->Version); $this->domain = $config['domain']; } @@ -257,77 +261,21 @@ class aliyun implements DnsInterface return $line; } - - private function aliyunSignature($parameters, $accessKeySecret, $method) - { - ksort($parameters); - $canonicalizedQueryString = ''; - foreach ($parameters as $key => $value) { - if ($value === null) continue; - $canonicalizedQueryString .= '&' . $this->percentEncode($key) . '=' . $this->percentEncode($value); - } - $stringToSign = $method . '&%2F&' . $this->percentEncode(substr($canonicalizedQueryString, 1)); - $signature = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&", true)); - - return $signature; - } - private function percentEncode($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } private function request($param, $returnData = false) { if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) return false; - $result = $this->request_do($param, $returnData); - if (!$returnData && $result !== true) { - usleep(50000); - $result = $this->request_do($param, $returnData); - } - return $result; - } - private function request_do($param, $returnData = false) - { - if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) return false; - $url = 'https://' . $this->Endpoint . '/'; - $data = array( - 'Format' => 'JSON', - 'Version' => $this->Version, - 'AccessKeyId' => $this->AccessKeyId, - 'SignatureMethod' => 'HMAC-SHA1', - 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), - 'SignatureVersion' => '1.0', - 'SignatureNonce' => random(8) - ); - $data = array_merge($data, $param); - $data['Signature'] = $this->aliyunSignature($data, $this->AccessKeySecret, 'POST'); - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); - $response = curl_exec($ch); - $errno = curl_errno($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($errno) { - $this->setError('Curl error: ' . curl_error($ch)); - } - curl_close($ch); - if ($errno) return false; - - $arr = json_decode($response, true); - if ($httpCode == 200) { - return $returnData ? $arr : true; - } elseif ($arr) { - $this->setError($arr['Message']); - return false; - } else { - $this->setError('返回数据解析失败'); - return false; + try{ + $result = $this->client->request($param); + }catch(Exception $e){ + try{ + usleep(50000); + $result = $this->client->request($param); + }catch(Exception $e){ + $this->setError($e->getMessage()); + return false; + } } + return $returnData ? $result : true; } private function setError($message) diff --git a/app/lib/dns/baidu.php b/app/lib/dns/baidu.php index bfc9617..4f0e500 100644 --- a/app/lib/dns/baidu.php +++ b/app/lib/dns/baidu.php @@ -3,6 +3,8 @@ namespace app\lib\dns; use app\lib\DnsInterface; +use app\lib\client\BaiduCloud; +use Exception; class baidu implements DnsInterface { @@ -12,11 +14,13 @@ class baidu implements DnsInterface private $error; private $domain; private $domainid; + private BaiduCloud $client; public function __construct($config) { $this->AccessKeyId = $config['ak']; $this->SecretAccessKey = $config['sk']; + $this->client = new BaiduCloud($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint); $this->domain = $config['domain']; $this->domainid = $config['domainid']; } @@ -197,147 +201,10 @@ class baidu implements DnsInterface private function send_reuqest($method, $path, $query = null, $params = null) { - if (!empty($query)) { - $query = array_filter($query, function ($a) { return $a !== null;}); - } - if (!empty($params)) { - $params = array_filter($params, function ($a) { return $a !== null;}); - } - - $time = time(); - $date = gmdate("Y-m-d\TH:i:s\Z", $time); - $body = !empty($params) ? json_encode($params) : ''; - $headers = [ - 'Host' => $this->endpoint, - 'x-bce-date' => $date, - ]; - if ($body) { - $headers['Content-Type'] = 'application/json'; - } - - $authorization = $this->generateSign($method, $path, $query, $headers, $time); - $headers['Authorization'] = $authorization; - - $url = 'https://'.$this->endpoint.$path; - if (!empty($query)) { - $url .= '?'.http_build_query($query); - } - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key.': '.$value; - } - return $this->curl($method, $url, $body, $header); - } - - private function generateSign($method, $path, $query, $headers, $time) - { - $algorithm = "bce-auth-v1"; - - // step 1: build canonical request string - $httpRequestMethod = $method; - $canonicalUri = $this->getCanonicalUri($path); - $canonicalQueryString = $this->getCanonicalQueryString($query); - [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); - $canonicalRequest = $httpRequestMethod."\n" - .$canonicalUri."\n" - .$canonicalQueryString."\n" - .$canonicalHeaders; - - // step 2: calculate signing key - $date = gmdate("Y-m-d\TH:i:s\Z", $time); - $expirationInSeconds = 1800; - $authString = $algorithm . '/' . $this->AccessKeyId . '/' . $date . '/' . $expirationInSeconds; - $signingKey = hash_hmac('sha256', $authString, $this->SecretAccessKey); - - // step 3: sign string - $signature = hash_hmac("sha256", $canonicalRequest, $signingKey); - - // step 4: build authorization - $authorization = $authString . '/' . $signedHeaders . "/" . $signature; - - return $authorization; - } - - private function escape($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } - - private function getCanonicalUri($path) - { - if (empty($path)) return '/'; - $uri = str_replace('%2F', '/', $this->escape($path)); - if (substr($uri, 0, 1) !== '/') $uri = '/' . $uri; - return $uri; - } - - private function getCanonicalQueryString($parameters) - { - if (empty($parameters)) return ''; - ksort($parameters); - $canonicalQueryString = ''; - foreach ($parameters as $key => $value) { - if ($key == 'authorization') continue; - $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); - } - return substr($canonicalQueryString, 1); - } - - private function getCanonicalHeaders($oldheaders) - { - $headers = array(); - foreach ($oldheaders as $key => $value) { - $headers[strtolower($key)] = trim($value); - } - ksort($headers); - - $canonicalHeaders = ''; - $signedHeaders = ''; - foreach ($headers as $key => $value) { - $canonicalHeaders .= $this->escape($key) . ':' . $this->escape($value) . "\n"; - $signedHeaders .= $key . ';'; - } - $canonicalHeaders = substr($canonicalHeaders, 0, -1); - $signedHeaders = substr($signedHeaders, 0, -1); - return [$canonicalHeaders, $signedHeaders]; - } - - private function curl($method, $url, $body, $header) - { - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - if (!empty($body)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - $response = curl_exec($ch); - $errno = curl_errno($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($errno) { - $this->setError('Curl error: ' . curl_error($ch)); - } - curl_close($ch); - if ($errno) return false; - - if (empty($response) && $httpCode == 200) { - return true; - } - $arr = json_decode($response, true); - if ($arr) { - if (isset($arr['code']) && isset($arr['message'])) { - $this->setError($arr['message']); - return false; - } else { - return $arr; - } - } else { - $this->setError('返回数据解析失败'); + try{ + return $this->client->request($method, $path, $query, $params); + }catch(Exception $e){ + $this->setError($e->getMessage()); return false; } } diff --git a/app/lib/dns/dnspod.php b/app/lib/dns/dnspod.php index 9a079b3..78ddc31 100644 --- a/app/lib/dns/dnspod.php +++ b/app/lib/dns/dnspod.php @@ -3,6 +3,8 @@ namespace app\lib\dns; use app\lib\DnsInterface; +use app\lib\client\TencentCloud; +use Exception; class dnspod implements DnsInterface { @@ -15,11 +17,13 @@ class dnspod implements DnsInterface private $domain; private $domainid; private $domainInfo; + private TencentCloud $client; public function __construct($config) { $this->SecretId = $config['ak']; $this->SecretKey = $config['sk']; + $this->client = new TencentCloud($this->SecretId, $this->SecretKey, $this->endpoint, $this->service, $this->version); $this->domain = $config['domain']; } @@ -42,7 +46,7 @@ class dnspod implements DnsInterface $action = 'DescribeDomainList'; $offset = ($PageNumber - 1) * $PageSize; $param = ['Offset' => $offset, 'Limit' => $PageSize, 'Keyword' => $KeyWord]; - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); if ($data) { $list = []; foreach ($data['DomainList'] as $row) { @@ -77,7 +81,7 @@ class dnspod implements DnsInterface $action = 'DescribeRecordList'; $param = ['Domain' => $this->domain, 'Subdomain' => $SubDomain, 'RecordType' => $this->convertType($Type), 'RecordLineId' => $Line, 'Keyword' => $KeyWord, 'Offset' => $offset, 'Limit' => $PageSize]; } - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); if ($data) { $list = []; foreach ($data['RecordList'] as $row) { @@ -116,7 +120,7 @@ class dnspod implements DnsInterface { $action = 'DescribeRecord'; $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId)]; - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); if ($data) { return [ 'RecordId' => $data['RecordInfo']['Id'], @@ -142,7 +146,7 @@ class dnspod implements DnsInterface $action = 'CreateRecord'; $param = ['Domain' => $this->domain, 'SubDomain' => $Name, 'RecordType' => $this->convertType($Type), 'Value' => $Value, 'RecordLine' => $Line, 'RecordLineId' => $this->convertLineCode($Line), 'TTL' => intval($TTL), 'Weight' => $Weight]; if ($Type == 'MX') $param['MX'] = intval($MX); - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); return is_array($data) ? $data['RecordId'] : false; } @@ -152,7 +156,7 @@ class dnspod implements DnsInterface $action = 'ModifyRecord'; $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId), 'SubDomain' => $Name, 'RecordType' => $this->convertType($Type), 'Value' => $Value, 'RecordLine' => $Line, 'RecordLineId' => $this->convertLineCode($Line), 'TTL' => intval($TTL), 'Weight' => $Weight]; if ($Type == 'MX') $param['MX'] = intval($MX); - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); return is_array($data); } @@ -161,7 +165,7 @@ class dnspod implements DnsInterface { $action = 'ModifyRecordRemark'; $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId), 'Remark' => $Remark]; - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); return is_array($data); } @@ -170,7 +174,7 @@ class dnspod implements DnsInterface { $action = 'DeleteRecord'; $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId)]; - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); return is_array($data); } @@ -180,7 +184,7 @@ class dnspod implements DnsInterface $Status = $Status == '1' ? 'ENABLE' : 'DISABLE'; $action = 'ModifyRecordStatus'; $param = ['Domain' => $this->domain, 'RecordId' => intval($RecordId), 'Status' => $Status]; - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); return is_array($data); } @@ -190,7 +194,7 @@ class dnspod implements DnsInterface $action = 'DescribeDomainLogList'; $offset = ($PageNumber - 1) * $PageSize; $param = ['Domain' => $this->domain, 'Offset' => $offset, 'Limit' => $PageSize]; - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); if ($data) { $list = []; foreach ($data['LogList'] as $row) { @@ -206,7 +210,7 @@ class dnspod implements DnsInterface { $action = 'DescribeRecordLineCategoryList'; $param = ['Domain' => $this->domain]; - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); if ($data) { $list = []; $this->processLineList($list, $data['LineList'], null); @@ -242,7 +246,7 @@ class dnspod implements DnsInterface { $action = 'DescribeDomain'; $param = ['Domain' => $this->domain]; - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); if ($data) { $this->domainInfo = $data['DomainInfo']; return $data['DomainInfo']; @@ -255,7 +259,7 @@ class dnspod implements DnsInterface { $action = 'DescribeDomainPurview'; $param = ['Domain' => $this->domain]; - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); if ($data) { return $data['PurviewList']; } @@ -284,7 +288,7 @@ class dnspod implements DnsInterface { $action = 'DescribeRecordLineList'; $param = ['Domain' => $this->domain, 'DomainGrade' => '']; - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); if ($data) { $line_list = $data['LineList']; if (!empty($data['LineGroupList'])) { @@ -302,7 +306,7 @@ class dnspod implements DnsInterface { $action = 'DescribeUserDetail'; $param = []; - $data = $this->send_reuqest($action, $param); + $data = $this->send_request($action, $param); if ($data) { return $data['UserInfo']; } @@ -337,93 +341,12 @@ class dnspod implements DnsInterface } - private function send_reuqest($action, $param) + private function send_request($action, $param) { - $param = array_filter($param, function ($a) { return $a !== null;}); - if (!$param) $param = (object)[]; - $payload = json_encode($param); - $time = time(); - $authorization = $this->generateSign($payload, $time); - $header = [ - 'Authorization: '.$authorization, - 'Content-Type: application/json; charset=utf-8', - 'X-TC-Action: '.$action, - 'X-TC-Timestamp: '.$time, - 'X-TC-Version: '.$this->version, - ]; - return $this->curl_post($payload, $header); - } - - private function generateSign($payload, $time) - { - $algorithm = "TC3-HMAC-SHA256"; - - // step 1: build canonical request string - $httpRequestMethod = "POST"; - $canonicalUri = "/"; - $canonicalQueryString = ""; - $canonicalHeaders = "content-type:application/json; charset=utf-8\n"."host:".$this->endpoint."\n"; - $signedHeaders = "content-type;host"; - $hashedRequestPayload = hash("SHA256", $payload); - $canonicalRequest = $httpRequestMethod."\n" - .$canonicalUri."\n" - .$canonicalQueryString."\n" - .$canonicalHeaders."\n" - .$signedHeaders."\n" - .$hashedRequestPayload; - - // step 2: build string to sign - $date = gmdate("Y-m-d", $time); - $credentialScope = $date."/".$this->service."/tc3_request"; - $hashedCanonicalRequest = hash("SHA256", $canonicalRequest); - $stringToSign = $algorithm."\n" - .$time."\n" - .$credentialScope."\n" - .$hashedCanonicalRequest; - - // step 3: sign string - $secretDate = hash_hmac("SHA256", $date, "TC3".$this->SecretKey, true); - $secretService = hash_hmac("SHA256", $this->service, $secretDate, true); - $secretSigning = hash_hmac("SHA256", "tc3_request", $secretService, true); - $signature = hash_hmac("SHA256", $stringToSign, $secretSigning); - - // step 4: build authorization - $authorization = $algorithm - ." Credential=".$this->SecretId."/".$credentialScope - .", SignedHeaders=content-type;host, Signature=".$signature; - - return $authorization; - } - - private function curl_post($payload, $header) - { - $url = 'https://'.$this->endpoint.'/'; - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); - $response = curl_exec($ch); - $errno = curl_errno($ch); - if ($errno) { - $this->setError('Curl error: ' . curl_error($ch)); - } - curl_close($ch); - if ($errno) return false; - - $arr = json_decode($response, true); - if ($arr) { - if (isset($arr['Response']['Error'])) { - $this->setError($arr['Response']['Error']['Message']); - return false; - } else { - return $arr['Response']; - } - } else { - $this->setError('返回数据解析失败'); + try{ + return $this->client->request($action, $param); + }catch(Exception $e){ + $this->setError($e->getMessage()); return false; } } diff --git a/app/lib/dns/huawei.php b/app/lib/dns/huawei.php index 7548a31..2bd4bd9 100644 --- a/app/lib/dns/huawei.php +++ b/app/lib/dns/huawei.php @@ -3,6 +3,8 @@ namespace app\lib\dns; use app\lib\DnsInterface; +use app\lib\client\HuaweiCloud; +use Exception; class huawei implements DnsInterface { @@ -12,11 +14,13 @@ class huawei implements DnsInterface private $error; private $domain; private $domainid; + private HuaweiCloud $client; public function __construct($config) { $this->AccessKeyId = $config['ak']; $this->SecretAccessKey = $config['sk']; + $this->client = new HuaweiCloud($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint); $this->domain = $config['domain']; $this->domainid = $config['domainid']; } @@ -39,7 +43,7 @@ class huawei implements DnsInterface { $offset = ($PageNumber - 1) * $PageSize; $query = ['offset' => $offset, 'limit' => $PageSize, 'name' => $KeyWord]; - $data = $this->send_reuqest('GET', '/v2/zones', $query); + $data = $this->send_request('GET', '/v2/zones', $query); if ($data) { $list = []; foreach ($data['zones'] as $row) { @@ -68,7 +72,7 @@ class huawei implements DnsInterface $query['name'] = $SubDomain; $query['search_mode'] = 'equal'; } - $data = $this->send_reuqest('GET', '/v2.1/zones/'.$this->domainid.'/recordsets', $query); + $data = $this->send_request('GET', '/v2.1/zones/'.$this->domainid.'/recordsets', $query); if ($data) { $list = []; foreach ($data['recordsets'] as $row) { @@ -103,7 +107,7 @@ class huawei implements DnsInterface //获取解析记录详细信息 public function getDomainRecordInfo($RecordId) { - $data = $this->send_reuqest('GET', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId); + $data = $this->send_request('GET', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId); if ($data) { if ($data['name'] == $data['zone_name']) $data['name'] = '@'; if ($data['type'] == 'MX') list($data['mx'], $data['records']) = explode(' ', $data['records'][0]); @@ -134,7 +138,7 @@ class huawei implements DnsInterface $params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => $records, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark]; if ($Type == 'MX') $params['records'][0] = intval($MX) . ' ' . $Value; if ($Weight > 0) $params['weight'] = intval($Weight); - $data = $this->send_reuqest('POST', '/v2.1/zones/'.$this->domainid.'/recordsets', null, $params); + $data = $this->send_request('POST', '/v2.1/zones/'.$this->domainid.'/recordsets', null, $params); return is_array($data) ? $data['id'] : false; } @@ -147,7 +151,7 @@ class huawei implements DnsInterface $params = ['name' => $Name, 'type' => $this->convertType($Type), 'records' => $records, 'line' => $Line, 'ttl' => intval($TTL), 'description' => $Remark]; if ($Type == 'MX') $params['records'][0] = intval($MX) . ' ' . $Value; if ($Weight > 0) $params['weight'] = intval($Weight); - $data = $this->send_reuqest('PUT', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId, null, $params); + $data = $this->send_request('PUT', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId, null, $params); return is_array($data); } @@ -160,7 +164,7 @@ class huawei implements DnsInterface //删除解析记录 public function deleteDomainRecord($RecordId) { - $data = $this->send_reuqest('DELETE', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId); + $data = $this->send_request('DELETE', '/v2.1/zones/'.$this->domainid.'/recordsets/'.$RecordId); return is_array($data); } @@ -169,7 +173,7 @@ class huawei implements DnsInterface { $Status = $Status == '1' ? 'ENABLE' : 'DISABLE'; $params = ['status' => $Status]; - $data = $this->send_reuqest('PUT', '/v2.1/recordsets/'.$RecordId.'/statuses/set', null, $params); + $data = $this->send_request('PUT', '/v2.1/recordsets/'.$RecordId.'/statuses/set', null, $params); return is_array($data); } @@ -215,7 +219,7 @@ class huawei implements DnsInterface //获取域名概览信息 public function getDomainInfo() { - return $this->send_reuqest('GET', '/v2/zones/'.$this->domainid); + return $this->send_request('GET', '/v2/zones/'.$this->domainid); } //获取域名最低TTL @@ -237,143 +241,12 @@ class huawei implements DnsInterface return $Name; } - private function send_reuqest($method, $path, $query = null, $params = null) + private function send_request($method, $path, $query = null, $params = null) { - if (!empty($query)) { - $query = array_filter($query, function ($a) { return $a !== null;}); - } - if (!empty($params)) { - $params = array_filter($params, function ($a) { return $a !== null;}); - } - - $time = time(); - $date = gmdate("Ymd\THis\Z", $time); - $body = !empty($params) ? json_encode($params) : ''; - $headers = [ - 'Host' => $this->endpoint, - 'X-Sdk-Date' => $date, - ]; - if ($body) { - $headers['Content-Type'] = 'application/json'; - } - - $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); - $headers['Authorization'] = $authorization; - - $url = 'https://'.$this->endpoint.$path; - if (!empty($query)) { - $url .= '?'.http_build_query($query); - } - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key.': '.$value; - } - return $this->curl($method, $url, $body, $header); - } - - private function generateSign($method, $path, $query, $headers, $body, $time) - { - $algorithm = "SDK-HMAC-SHA256"; - - // step 1: build canonical request string - $httpRequestMethod = $method; - $canonicalUri = $path; - if (substr($canonicalUri, -1) != "/") $canonicalUri .= "/"; - $canonicalQueryString = $this->getCanonicalQueryString($query); - [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); - $hashedRequestPayload = hash("sha256", $body); - $canonicalRequest = $httpRequestMethod."\n" - .$canonicalUri."\n" - .$canonicalQueryString."\n" - .$canonicalHeaders."\n" - .$signedHeaders."\n" - .$hashedRequestPayload; - - // step 2: build string to sign - $date = gmdate("Ymd\THis\Z", $time); - $hashedCanonicalRequest = hash("sha256", $canonicalRequest); - $stringToSign = $algorithm."\n" - .$date."\n" - .$hashedCanonicalRequest; - - // step 3: sign string - $signature = hash_hmac("sha256", $stringToSign, $this->SecretAccessKey); - - // step 4: build authorization - $authorization = $algorithm . ' Access=' . $this->AccessKeyId . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; - - return $authorization; - } - - private function escape($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } - - private function getCanonicalQueryString($parameters) - { - if (empty($parameters)) return ''; - ksort($parameters); - $canonicalQueryString = ''; - foreach ($parameters as $key => $value) { - $canonicalQueryString .= '&' . $this->escape($key). '=' . $this->escape($value); - } - return substr($canonicalQueryString, 1); - } - - private function getCanonicalHeaders($oldheaders) - { - $headers = array(); - foreach ($oldheaders as $key => $value) { - $headers[strtolower($key)] = trim($value); - } - ksort($headers); - - $canonicalHeaders = ''; - $signedHeaders = ''; - foreach ($headers as $key => $value) { - $canonicalHeaders .= $key . ':' . $value . "\n"; - $signedHeaders .= $key . ';'; - } - $signedHeaders = substr($signedHeaders, 0, -1); - return [$canonicalHeaders, $signedHeaders]; - } - - private function curl($method, $url, $body, $header) - { - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - if (!empty($body)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - $response = curl_exec($ch); - $errno = curl_errno($ch); - if ($errno) { - $this->setError('Curl error: ' . curl_error($ch)); - } - curl_close($ch); - if ($errno) return false; - - $arr = json_decode($response, true); - if ($arr) { - if (isset($arr['error_msg'])) { - $this->setError($arr['error_msg']); - return false; - } elseif (isset($arr['message'])) { - $this->setError($arr['message']); - return false; - } else { - return $arr; - } - } else { - $this->setError('返回数据解析失败'); + try{ + return $this->client->request($method, $path, $query, $params); + }catch(Exception $e){ + $this->setError($e->getMessage()); return false; } } diff --git a/app/lib/dns/huoshan.php b/app/lib/dns/huoshan.php index e56e9e0..71dd792 100644 --- a/app/lib/dns/huoshan.php +++ b/app/lib/dns/huoshan.php @@ -3,6 +3,8 @@ namespace app\lib\dns; use app\lib\DnsInterface; +use app\lib\client\Volcengine; +use Exception; class huoshan implements DnsInterface { @@ -16,6 +18,7 @@ class huoshan implements DnsInterface private $domain; private $domainid; private $domainInfo; + private Volcengine $client; private static $trade_code_list = [ 'free_inner' => ['level' => 1, 'name' => '免费版', 'ttl' => 600], @@ -29,6 +32,7 @@ class huoshan implements DnsInterface { $this->AccessKeyId = $config['ak']; $this->SecretAccessKey = $config['sk']; + $this->client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, $this->endpoint, $this->service, $this->version, $this->region); $this->domain = $config['domain']; $this->domainid = $config['domainid']; } @@ -50,7 +54,7 @@ class huoshan implements DnsInterface public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20) { $query = ['PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'Key' => $KeyWord]; - $data = $this->send_reuqest('GET', 'ListZones', $query); + $data = $this->send_request('GET', 'ListZones', $query); if ($data) { $list = []; if (!empty($data['Zones'])) { @@ -76,7 +80,7 @@ class huoshan implements DnsInterface } elseif (!empty($KeyWord)) { $query += ['Host' => $KeyWord]; } - $data = $this->send_reuqest('GET', 'ListRecords', $query); + $data = $this->send_request('GET', 'ListRecords', $query); if ($data) { $list = []; foreach ($data['Records'] as $row) { @@ -110,7 +114,7 @@ class huoshan implements DnsInterface //获取解析记录详细信息 public function getDomainRecordInfo($RecordId) { - $data = $this->send_reuqest('GET', 'QueryRecord', ['RecordID' => $RecordId]); + $data = $this->send_request('GET', 'QueryRecord', ['RecordID' => $RecordId]); if ($data) { if ($data['name'] == $data['zone_name']) $data['name'] = '@'; if ($data['Type'] == 'MX') list($data['MX'], $data['Value']) = explode(' ', $data['Value']); @@ -138,7 +142,7 @@ class huoshan implements DnsInterface $params = ['ZID' => intval($this->domainid), 'Host' => $Name, 'Type' => $this->convertType($Type), 'Value' => $Value, 'Line' => $Line, 'TTL' => intval($TTL), 'Remark' => $Remark]; if ($Type == 'MX') $params['Value'] = intval($MX) . ' ' . $Value; if ($Weight > 0) $params['Weight'] = $Weight; - $data = $this->send_reuqest('POST', 'CreateRecord', $params); + $data = $this->send_request('POST', 'CreateRecord', $params); return is_array($data) ? $data['RecordID'] : false; } @@ -148,7 +152,7 @@ class huoshan implements DnsInterface $params = ['RecordID' => $RecordId, 'Host' => $Name, 'Type' => $this->convertType($Type), 'Value' => $Value, 'Line' => $Line, 'TTL' => intval($TTL), 'Remark' => $Remark]; if ($Type == 'MX') $params['Value'] = intval($MX) . ' ' . $Value; if ($Weight > 0) $params['Weight'] = $Weight; - $data = $this->send_reuqest('POST', 'UpdateRecord', $params); + $data = $this->send_request('POST', 'UpdateRecord', $params); return is_array($data); } @@ -161,7 +165,7 @@ class huoshan implements DnsInterface //删除解析记录 public function deleteDomainRecord($RecordId) { - $data = $this->send_reuqest('POST', 'DeleteRecord', ['RecordID' => $RecordId]); + $data = $this->send_request('POST', 'DeleteRecord', ['RecordID' => $RecordId]); return $data; } @@ -169,7 +173,7 @@ class huoshan implements DnsInterface public function setDomainRecordStatus($RecordId, $Status) { $params = ['RecordID' => $RecordId, 'Enable' => $Status == '1']; - $data = $this->send_reuqest('POST', 'UpdateRecordStatus', $params); + $data = $this->send_request('POST', 'UpdateRecordStatus', $params); return is_array($data); } @@ -185,7 +189,7 @@ class huoshan implements DnsInterface $domainInfo = $this->getDomainInfo(); if (!$domainInfo) return false; $level = $this->getTradeInfo($domainInfo['TradeCode'])['level']; - $data = $this->send_reuqest('GET', 'ListLines', []); + $data = $this->send_request('GET', 'ListLines', []); if ($data) { $list = []; $list['default'] = ['name' => '默认', 'parent' => null]; @@ -195,7 +199,7 @@ class huoshan implements DnsInterface $list[$row['Value']] = ['name' => $row['Name'], 'parent' => isset($row['FatherValue']) ? $row['FatherValue'] : null]; } - $data = $this->send_reuqest('GET', 'ListCustomLines', []); + $data = $this->send_request('GET', 'ListCustomLines', []); if ($data && $data['TotalCount'] > 0) { $list['N.customer_lines'] = ['name' => '自定义线路', 'parent' => null]; foreach ($data['CustomerLines'] as $row) { @@ -213,7 +217,7 @@ class huoshan implements DnsInterface { if (!empty($this->domainInfo)) return $this->domainInfo; $query = ['ZID' => intval($this->domainid)]; - $data = $this->send_reuqest('GET', 'QueryZone', $query); + $data = $this->send_request('GET', 'QueryZone', $query); if ($data) { $this->domainInfo = $data; return $data; @@ -247,159 +251,12 @@ class huoshan implements DnsInterface return self::$trade_code_list[$trade_code]; } - private function send_reuqest($method, $action, $params = []) + private function send_request($method, $action, $params = []) { - if (!empty($params)) { - $params = array_filter($params, function ($a) { return $a !== null;}); - } - - $query = [ - 'Action' => $action, - 'Version' => $this->version, - ]; - - $body = ''; - if ($method == 'GET') { - $query = array_merge($query, $params); - } else { - $body = !empty($params) ? json_encode($params) : ''; - } - - $time = time(); - $headers = [ - 'Host' => $this->endpoint, - 'X-Date' => gmdate("Ymd\THis\Z", $time), - //'X-Content-Sha256' => hash("sha256", $body), - ]; - if ($body) { - $headers['Content-Type'] = 'application/json'; - } - $path = '/'; - - $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); - $headers['Authorization'] = $authorization; - - $url = 'https://'.$this->endpoint.$path.'?'.http_build_query($query); - $header = []; - foreach ($headers as $key => $value) { - $header[] = $key.': '.$value; - } - return $this->curl($method, $url, $body, $header); - } - - private function generateSign($method, $path, $query, $headers, $body, $time) - { - $algorithm = "HMAC-SHA256"; - - // step 1: build canonical request string - $httpRequestMethod = $method; - $canonicalUri = $path; - if (substr($canonicalUri, -1) != "/") $canonicalUri .= "/"; - $canonicalQueryString = $this->getCanonicalQueryString($query); - [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); - $hashedRequestPayload = hash("sha256", $body); - $canonicalRequest = $httpRequestMethod."\n" - .$canonicalUri."\n" - .$canonicalQueryString."\n" - .$canonicalHeaders."\n" - .$signedHeaders."\n" - .$hashedRequestPayload; - - // step 2: build string to sign - $date = gmdate("Ymd\THis\Z", $time); - $shortDate = substr($date, 0, 8); - $credentialScope = $shortDate . '/' .$this->region . '/' . $this->service . '/request'; - $hashedCanonicalRequest = hash("sha256", $canonicalRequest); - $stringToSign = $algorithm."\n" - .$date."\n" - .$credentialScope."\n" - .$hashedCanonicalRequest; - - // step 3: sign string - $kDate = hash_hmac("sha256", $shortDate, $this->SecretAccessKey, true); - $kRegion = hash_hmac("sha256", $this->region, $kDate, true); - $kService = hash_hmac("sha256", $this->service, $kRegion, true); - $kSigning = hash_hmac("sha256", "request", $kService, true); - $signature = hash_hmac("sha256", $stringToSign, $kSigning); - - // step 4: build authorization - $credential = $this->AccessKeyId . '/' . $shortDate . '/' . $this->region . '/' . $this->service . '/request'; - $authorization = $algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; - - return $authorization; - } - - private function escape($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } - - private function getCanonicalQueryString($parameters) - { - if (empty($parameters)) return ''; - ksort($parameters); - $canonicalQueryString = ''; - foreach ($parameters as $key => $value) { - $canonicalQueryString .= '&' . $this->escape($key). '=' . $this->escape($value); - } - return substr($canonicalQueryString, 1); - } - - private function getCanonicalHeaders($oldheaders) - { - $headers = array(); - foreach ($oldheaders as $key => $value) { - $headers[strtolower($key)] = trim($value); - } - ksort($headers); - - $canonicalHeaders = ''; - $signedHeaders = ''; - foreach ($headers as $key => $value) { - $canonicalHeaders .= $key . ':' . $value . "\n"; - $signedHeaders .= $key . ';'; - } - $signedHeaders = substr($signedHeaders, 0, -1); - return [$canonicalHeaders, $signedHeaders]; - } - - private function curl($method, $url, $body, $header) - { - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - if (!empty($body)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - $response = curl_exec($ch); - $errno = curl_errno($ch); - if ($errno) { - $this->setError('Curl error: ' . curl_error($ch)); - } - curl_close($ch); - if ($errno) return false; - - $arr = json_decode($response, true); - if ($arr) { - if (isset($arr['ResponseMetadata']['Error']['MessageCN'])) { - $this->setError($arr['ResponseMetadata']['Error']['MessageCN']); - return false; - } elseif (isset($arr['ResponseMetadata']['Error']['Message'])) { - $this->setError($arr['ResponseMetadata']['Error']['Message']); - return false; - } elseif (isset($arr['Result'])) { - return $arr['Result']; - } else { - return true; - } - } else { - $this->setError('返回数据解析失败'); + try{ + return $this->client->request($method, $action, $params); + }catch(Exception $e){ + $this->setError($e->getMessage()); return false; } } diff --git a/app/lib/mail/Aliyun.php b/app/lib/mail/Aliyun.php index 058625b..5083644 100644 --- a/app/lib/mail/Aliyun.php +++ b/app/lib/mail/Aliyun.php @@ -2,40 +2,27 @@ namespace app\lib\mail; +use app\lib\client\Aliyun as AliyunClient; + class Aliyun { private $AccessKeyId; private $AccessKeySecret; + private $Endpoint = 'dm.aliyuncs.com'; + private $Version = '2015-11-23'; + private AliyunClient $client; public function __construct($AccessKeyId, $AccessKeySecret) { $this->AccessKeyId = $AccessKeyId; $this->AccessKeySecret = $AccessKeySecret; + $this->client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $this->Endpoint, $this->Version); } - private function aliyunSignature($parameters, $accessKeySecret, $method) - { - ksort($parameters); - $canonicalizedQueryString = ''; - foreach ($parameters as $key => $value) { - if ($value === null) continue; - $canonicalizedQueryString .= '&' . $this->percentEncode($key) . '=' . $this->percentEncode($value); - } - $stringToSign = $method . '&%2F&' . $this->percentencode(substr($canonicalizedQueryString, 1)); - $signature = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&", true)); - return $signature; - } - private function percentEncode($str) - { - $search = ['+', '*', '%7E']; - $replace = ['%20', '%2A', '~']; - return str_replace($search, $replace, urlencode($str)); - } public function send($to, $sub, $msg, $from, $from_name) { if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) return false; - $url = 'https://dm.aliyuncs.com/'; - $data = array( + $param = [ 'Action' => 'SingleSendMail', 'AccountName' => $from, 'ReplyToAddress' => 'false', @@ -44,30 +31,12 @@ class Aliyun 'FromAlias' => $from_name, 'Subject' => $sub, 'HtmlBody' => $msg, - 'Format' => 'JSON', - 'Version' => '2015-11-23', - 'AccessKeyId' => $this->AccessKeyId, - 'SignatureMethod' => 'HMAC-SHA1', - 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), - 'SignatureVersion' => '1.0', - 'SignatureNonce' => random(8) - ); - $data['Signature'] = $this->aliyunSignature($data, $this->AccessKeySecret, 'POST'); - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); - $json = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - $arr = json_decode($json, true); - if ($httpCode == 200) { + ]; + try { + $this->client->request($param); return true; - } else { - return $arr['Message']; + } catch (\Exception $e) { + return $e->getMessage(); } } } diff --git a/app/middleware/AuthUser.php b/app/middleware/AuthUser.php index a953ef3..3000e51 100644 --- a/app/middleware/AuthUser.php +++ b/app/middleware/AuthUser.php @@ -21,7 +21,7 @@ class AuthUser $user = Db::name('user')->where('id', $uid)->find(); if ($user && $user['status'] == 1) { $session = md5($user['id'].$user['password']); - if ($session == $sid && $expiretime > time()) { + if ($session === $sid && $expiretime > time()) { $islogin = true; } $user['type'] = 'user'; @@ -34,7 +34,7 @@ class AuthUser $user = Db::name('domain')->where('id', $uid)->find(); if ($user && $user['is_sso'] == 1) { $session = md5($user['id'].$user['name']); - if ($session == $sid && $expiretime > time()) { + if ($session === $sid && $expiretime > time()) { $islogin = true; } $user['username'] = $user['name']; diff --git a/app/middleware/ViewOutput.php b/app/middleware/ViewOutput.php index 2703a6a..46faaff 100644 --- a/app/middleware/ViewOutput.php +++ b/app/middleware/ViewOutput.php @@ -19,7 +19,7 @@ class ViewOutput { View::assign('islogin', $request->islogin); View::assign('user', $request->user); - View::assign('cdnpublic', 'https://s4.zstatic.net/ajax/libs/'); + View::assign('cdnpublic', 'https://cdnjs.webstatic.cn/ajax/libs/'); View::assign('skin', getAdminSkin()); return $next($request); } diff --git a/app/service/CertDeployService.php b/app/service/CertDeployService.php new file mode 100644 index 0000000..092cd41 --- /dev/null +++ b/app/service/CertDeployService.php @@ -0,0 +1,140 @@ +where('id', $tid)->find(); + if (!$task) throw new Exception('该自动部署任务不存在', 102); + $this->task = $task; + + $this->aid = $task['aid']; + $this->client = DeployHelper::getModel($this->aid); + if (!$this->client) throw new Exception('该自动部署任务类型不存在', 102); + + $this->info = $task['info'] ? json_decode($task['info'], true) : null; + } + + public function process($isManual = false) + { + if ($this->task['status'] >= 1) return; + if ($this->task['retry'] >= 6 && !$isManual) { + throw new Exception('已超出最大重试次数('.$this->task['error'].')', 103); + } + + $order = Db::name('cert_order')->where('id', $this->task['oid'])->find(); + if(!$order) throw new Exception('SSL证书订单不存在', 102); + if($order['status'] == 4) throw new Exception('SSL证书订单已吊销', 102); + if($order['status'] != 3) throw new Exception('SSL证书订单未完成签发', 102); + if(empty($order['fullchain']) || empty($order['privatekey'])) throw new Exception('SSL证书或私钥内容不存在', 102); + + $this->lockTaskData(); + try { + $this->deploy($order['fullchain'], $order['privatekey']); + } finally { + $this->unlockTaskData(); + } + } + + //部署证书 + public function deploy($fullchain, $privatekey) + { + $this->client->setLogger(function ($txt) { + $this->saveLog($txt); + }); + $this->saveLog(date('Y-m-d H:i:s')); + $config = json_decode($this->task['config'], true); + $config['domainList'] = Db::name('cert_domain')->where('oid', $this->task['oid'])->order('sort', 'asc')->column('domain'); + try { + $this->client->deploy($fullchain, $privatekey, $config, $this->info); + $this->saveResult(1); + $this->saveLog('[Success] 证书部署成功'); + } catch (Exception $e) { + $this->saveResult(-1, $e->getMessage(), date('Y-m-d H:i:s', time() + (array_key_exists($this->task['retry'], self::$retry_interval) ? self::$retry_interval[$this->task['retry']] : 3600))); + throw $e; + } finally { + if($this->info){ + Db::name('cert_deploy')->where('id', $this->task['id'])->update(['info' => json_encode($this->info)]); + } + } + } + + //重置任务 + public function reset() + { + Db::name('cert_deploy')->where('id', $this->task['id'])->data(['status' => 0, 'retry' => 0, 'retrytime' => null, 'issend' => 0])->update(); + //$file_name = app()->getRuntimePath().'log/'.$this->task['processid'].'.log'; + //if (file_exists($file_name)) unlink($file_name); + $this->task['status'] = 0; + $this->task['retry'] = 0; + } + + private function saveResult($status, $error = null, $retrytime = null) + { + $this->task['status'] = $status; + $update = ['status' => $status, 'error' => $error, 'retrytime' => $retrytime]; + if ($status == 1){ + $update['retry'] = 0; + $update['lasttime'] = date('Y-m-d H:i:s'); + } + $res = Db::name('cert_deploy')->where('id', $this->task['id'])->data($update); + if ($status < 0 || $retrytime) { + $this->task['retry']++; + $res->inc('retry'); + } + $res->update(); + if ($error) { + $this->saveLog('[Error] ' . $error); + } + } + + private function lockTaskData() + { + Db::startTrans(); + try { + $isLock = Db::name('cert_deploy')->where('id', $this->task['id'])->lock(true)->value('islock'); + if ($isLock == 1 && time() - strtotime($this->task['locktime']) < 3600) { + throw new Exception('部署任务处理中,请稍后再试'); + } + $update = ['islock' => 1, 'locktime' => date('Y-m-d H:i:s')]; + if (empty($this->task['processid'])) $this->task['processid'] = $update['processid'] = getSid(); + Db::name('cert_deploy')->where('id', $this->task['id'])->update($update); + Db::commit(); + } catch (Exception $e) { + Db::rollback(); + throw $e; + } + } + + private function unlockTaskData() + { + Db::name('cert_deploy')->where('id', $this->task['id'])->update(['islock' => 0]); + } + + private function saveLog($txt) + { + if (empty($this->task['processid'])) return; + $file_name = app()->getRuntimePath().'log/'.$this->task['processid'].'.log'; + file_put_contents($file_name, $txt . PHP_EOL, FILE_APPEND); + if(php_sapi_name() == 'cli'){ + echo $txt . PHP_EOL; + } + } +} \ No newline at end of file diff --git a/app/service/CertOrderService.php b/app/service/CertOrderService.php new file mode 100644 index 0000000..7b86da5 --- /dev/null +++ b/app/service/CertOrderService.php @@ -0,0 +1,426 @@ +where('id', $oid)->find(); + if (!$order) throw new Exception('该证书订单不存在', 102); + $this->order = $order; + + $this->aid = $order['aid']; + $account = Db::name('cert_account')->where('id', $this->aid)->find(); + if (!$account) throw new Exception('该证书账户不存在', 102); + $config = json_decode($account['config'], true); + $ext = $account['ext'] ? json_decode($account['ext'], true) : null; + $this->atype = $account['type']; + $this->client = CertHelper::getModel2($account['type'], $config, $ext); + if (!$this->client) throw new Exception('该证书类型不存在', 102); + + $domainList = Db::name('cert_domain')->where('oid', $oid)->order('sort', 'asc')->column('domain'); + if (!$domainList) throw new Exception('该证书订单没有绑定域名', 102); + $this->domainList = $domainList; + $this->info = $order['info'] ? json_decode($order['info'], true) : null; + $this->dnsList = $order['dns'] ? json_decode($order['dns'], true) : null; + } + + //执行证书申请 + public function process($isManual = false) + { + if ($this->order['status'] >= 3) return 3; + if ($this->order['retry2'] >= 3 && !$isManual) { + throw new Exception('已超出最大重试次数('.$this->order['error'].')', 103); + } + if ($this->order['status'] != 1 && $this->order['status'] != 2 && $this->order['retry'] >= 3 && !$isManual) { + if ($this->order['status'] == -2 || $this->order['status'] == -5 || $this->order['status'] == -6 || $this->order['status'] == -7) { + $this->cancel(); + if($this->order['status'] <= -5) $this->delDns(); + Db::name('cert_order')->where('id', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retrytime' => null, 'updatetime' => date('Y-m-d H:i:s')])->inc('retry2')->update(); + $this->order['status'] = 0; + $this->order['retry'] = 0; + } else { + throw new Exception('已超出最大重试次数('.$this->order['error'].')', 103); + } + } + + $cname = CertHelper::$cert_config[$this->atype]['cname']; + foreach($this->domainList as $domain){ + $mainDomain = getMainDomain($domain); + if (!Db::name('domain')->where('name', $mainDomain)->find()) { + $cname_row = Db::name('cert_cname')->where('domain', $domain)->where('status', 1)->find(); + if (!$cname || !$cname_row) { + $errmsg = '域名'.$domain.'未在本系统添加'; + Db::name('cert_order')->where('id', $this->order['id'])->data(['error'=>$errmsg]); + throw new Exception($errmsg, 103); + } else { + $this->cnameDomainList[] = $cname_row['id']; + } + } + } + + $this->lockOrder(); + try { + return $this->processOrder($isManual); + } finally { + $this->unlockOrder(); + if (($this->order['status'] == -2 || $this->order['status'] == -5 || $this->order['status'] == -6 || $this->order['status'] == -7) && $this->order['retry'] >= 3) { + Db::name('cert_order')->where('id', $this->order['id'])->data(['retrytime' => date('Y-m-d H:i:s', time() + 3600)])->update(); + } + } + } + + private function processOrder($isManual = false) + { + $this->client->setLogger(function ($txt) { + $this->saveLog($txt); + }); + // step1: 购买证书 + if ($this->order['status'] == 0 || $this->order['status'] == -1) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始购买证书'); + $this->buyCert(); + } + // step2: 创建订单 + if ($this->order['status'] == 0 || $this->order['status'] == -2) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始创建订单'); + $this->createOrder(); + } + // step3: 添加DNS + if ($isManual && $this->order['status'] == -3 && CertDnsUtils::verifyDns($this->dnsList)) { + $this->saveResult(1); + $this->saveLog('检测到DNS记录已添加成功'); + return 1; + } + if ($this->order['status'] == 0 || $this->order['status'] == -3) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始添加DNS记录'); + $this->addDns(); + $this->saveLog('添加DNS记录成功,请等待生效后进行验证...'); + Db::name('cert_order')->where('id', $this->order['id'])->update(['retrytime' => date('Y-m-d H:i:s', time() + 300)]); + return 1; + } + // step4: 查询DNS + if ($this->order['status'] == 1 || $this->order['status'] == -4) { + $this->verifyDns(); + } + // step5: 验证订单 + if ($this->order['status'] == 1 || $this->order['status'] == -5) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始验证订单'); + $this->authOrder(); + } + // step6: 查询验证结果 + if ($this->order['status'] == 2 || $this->order['status'] == -6) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始查询验证结果'); + $this->getAuthStatus(); + } + // step7: 签发证书 + if ($this->order['status'] == 2 || $this->order['status'] == -7) { + $this->saveLog(date('Y-m-d H:i:s').' - 开始签发证书'); + $this->finalizeOrder(); + } + $this->delDns(); + $this->resetRetry2(); + $this->saveLog('[Success] 证书签发成功'); + Db::name('cert_deploy')->where('oid', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retrytime' => null, 'issend' => 0])->update(); + return 3; + } + + private function lockOrder() + { + Db::startTrans(); + try { + $isLock = Db::name('cert_order')->where('id', $this->order['id'])->lock(true)->value('islock'); + if ($isLock == 1 && time() - strtotime($this->order['locktime']) < 3600) { + throw new Exception('订单正在处理中,请稍后再试', 102); + } + $update = ['islock' => 1, 'locktime' => date('Y-m-d H:i:s')]; + if (empty($this->order['processid'])) $this->order['processid'] = $update['processid'] = getSid(); + Db::name('cert_order')->where('id', $this->order['id'])->update($update); + Db::commit(); + } catch (Exception $e) { + Db::rollback(); + throw $e; + } + } + + private function unlockOrder() + { + Db::name('cert_order')->where('id', $this->order['id'])->update(['islock' => 0]); + } + + private function saveResult($status, $error = null, $retrytime = null) + { + $this->order['status'] = $status; + $update = ['status' => $status, 'error' => $error, 'updatetime' => date('Y-m-d H:i:s'), 'retrytime' => $retrytime]; + $res = Db::name('cert_order')->where('id', $this->order['id'])->data($update); + if ($status < 0 || $retrytime) { + $this->order['retry']++; + $res->inc('retry'); + } + $res->update(); + if ($error) { + $this->saveLog('[Error] ' . $error); + } + } + + private function resetRetry() + { + if ($this->order['retry'] > 0) { + $this->order['retry'] = 0; + Db::name('cert_order')->where('id', $this->order['id'])->update(['retry' => 0, 'retrytime' => null]); + } + } + + private function resetRetry2() + { + if ($this->order['retry2'] > 0) { + $this->order['retry2'] = 0; + Db::name('cert_order')->where('id', $this->order['id'])->update(['retry2' => 0, 'retrytime' => null]); + } + } + + //重置订单 + public function reset() + { + Db::name('cert_order')->where('id', $this->order['id'])->data(['status' => 0, 'retry' => 0, 'retry2' => 0, 'retrytime' => null, 'processid' => null, 'updatetime' => date('Y-m-d H:i:s'), 'issend' => 0])->update(); + $file_name = app()->getRuntimePath().'log/'.$this->order['processid'].'.log'; + if (file_exists($file_name)) unlink($file_name); + $this->order['status'] = 0; + $this->order['retry'] = 0; + $this->order['retry2'] = 0; + $this->order['processid'] = null; + } + + //购买证书 + public function buyCert() + { + try { + $this->client->buyCert($this->domainList, $this->info); + } catch (Exception $e) { + $this->saveResult(-1, $e->getMessage()); + throw $e; + } + if($this->info){ + Db::name('cert_order')->where('id', $this->order['id'])->update(['info' => json_encode($this->info)]); + } + $this->order['status'] = 0; + $this->resetRetry(); + } + + //创建订单 + public function createOrder() + { + try { + if (!empty($this->cnameDomainList)) { + foreach($this->cnameDomainList as $cnameId){ + $this->checkDomainCname($cnameId); + } + } + try { + $this->dnsList = $this->client->createOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']); + } catch (Exception $e) { + if (strpos($e->getMessage(), 'KeyID header contained an invalid account URL') !== false) { + $ext = $this->client->register(); + Db::name('cert_account')->where('id', $this->aid)->update(['ext' => json_encode($ext)]); + $this->dnsList = $this->client->createOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']); + } else { + throw $e; + } + } + } catch (Exception $e) { + $this->saveResult(-2, $e->getMessage()); + throw $e; + } + Db::name('cert_order')->where('id', $this->order['id'])->update(['info' => json_encode($this->info), 'dns' => json_encode($this->dnsList)]); + + if (!empty($this->dnsList)) { + $dns_txt = '需验证的DNS记录如下:'; + foreach ($this->dnsList as $mainDomain => $list) { + foreach ($list as $row) { + $domain = $row['name'] . '.' . $mainDomain; + $dns_txt .= PHP_EOL.'主机记录: '.$domain.' 类型: '.$row['type'].' 记录值: '.$row['value']; + } + } + $this->saveLog($dns_txt); + } + $this->order['status'] = 0; + $this->resetRetry(); + } + + //验证DNS记录 + public function verifyDns() + { + $verify = CertDnsUtils::verifyDns($this->dnsList); + if (!$verify) { + if ($this->order['retry'] >= 10) { + $this->saveResult(-4, '未查询到DNS解析记录'); + } else { + $this->saveLog('未查询到DNS解析记录(尝试第'.($this->order['retry']+1).'次)'); + $this->saveResult(1, null, date('Y-m-d H:i:s', time() + (array_key_exists($this->order['retry'], self::$retry_interval) ? self::$retry_interval[$this->order['retry']] : 1800))); + } + throw new Exception('未查询到DNS解析记录(尝试第'.($this->order['retry']).'次),请稍后再试'); + } + if($this->order['retry'] == 0 && time() - strtotime($this->order['updatetime']) < 10){ + throw new Exception('请等待'.(10 - (time() - strtotime($this->order['updatetime']))).'秒后再试'); + } + $this->order['status'] = 1; + $this->resetRetry(); + } + + //验证订单 + public function authOrder() + { + try { + $this->client->authOrder($this->domainList, $this->info); + } catch (Exception $e) { + $this->saveResult(-5, $e->getMessage()); + throw $e; + } + $this->saveResult(2); + $this->resetRetry(); + } + + //查询验证结果 + public function getAuthStatus() + { + try { + $status = $this->client->getAuthStatus($this->domainList, $this->info); + } catch (Exception $e) { + $this->saveResult(-6, $e->getMessage()); + throw $e; + } + if(!$status){ + if ($this->order['retry'] >= 10) { + $this->saveResult(-6, '订单验证未通过'); + } else { + $this->saveLog('订单验证未通过(尝试第'.($this->order['retry']+1).'次)'); + $this->saveResult(2, null, date('Y-m-d H:i:s', time() + (array_key_exists($this->order['retry'], self::$retry_interval) ? self::$retry_interval[$this->order['retry']] : 1800))); + } + throw new Exception('订单验证未通过(尝试第'.($this->order['retry']).'次),请稍后再试'); + } + $this->order['status'] = 2; + $this->resetRetry(); + } + + //签发证书 + public function finalizeOrder() + { + try { + $result = $this->client->finalizeOrder($this->domainList, $this->info, $this->order['keytype'], $this->order['keysize']); + } catch (Exception $e) { + $this->saveResult(-7, $e->getMessage()); + throw $e; + } + $this->order['issuer'] = $result['issuer']; + Db::name('cert_order')->where('id', $this->order['id'])->update(['fullchain' => $result['fullchain'], 'privatekey' => $result['private_key'], 'issuer' => $result['issuer'], 'issuetime' => date('Y-m-d H:i:s', $result['validFrom']), 'expiretime' => date('Y-m-d H:i:s', $result['validTo'])]); + $this->saveResult(3); + $this->resetRetry(); + } + + //吊销证书 + public function revoke() + { + $this->client->setLogger(function ($txt) { + $this->saveLog($txt); + }); + try { + $this->client->revoke($this->info, $this->order['fullchain']); + } catch (Exception $e) { + throw $e; + } + $this->saveResult(4); + } + + //取消证书订单 + public function cancel(){ + $this->client->setLogger(function ($txt) { + $this->saveLog($txt); + }); + if($this->order['status'] == 1 || $this->order['status'] == 2 || $this->order['status'] < -2){ + try { + $this->client->cancel($this->info); + } catch (Exception $e) { + } + } + } + + //添加DNS记录 + public function addDns() + { + if (empty($this->dnsList)) { + $this->saveResult(1); + return; + } + try { + CertDnsUtils::addDns($this->dnsList, function ($txt) { + $this->saveLog($txt); + }, !empty($this->cnameDomainList)); + } catch (Exception $e) { + $this->saveResult(-3, $e->getMessage()); + throw $e; + } + $this->saveResult(1); + $this->resetRetry(); + } + + //删除DNS记录 + public function delDns() + { + if (empty($this->dnsList)) return; + try { + CertDnsUtils::delDns($this->dnsList, function ($txt) { + $this->saveLog($txt); + }, true); + } catch (Exception $e) { + $this->saveLog('[Error] ' . $e->getMessage()); + } + } + + //检查域名CNAME代理记录 + private function checkDomainCname($id) + { + $row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.id', $id)->field('A.*,B.name cnamedomain')->find(); + $domain = '_acme-challenge.' . $row['domain']; + $record = $row['rr'] . '.' . $row['cnamedomain']; + $result = \app\utils\DnsQueryUtils::get_dns_records($domain, 'CNAME'); + if (!$result || !in_array($record, $result)) { + $result = \app\utils\DnsQueryUtils::query_dns_doh($domain, 'CNAME'); + if (!$result || !in_array($record, $result)) { + if ($row['status'] == 1) { + Db::name('cert_cname')->where('id', $id)->update(['status' => 0]); + } + throw new Exception('域名' . $row['domain'] . '的CNAME代理记录未验证通过'); + } + } + } + + private function saveLog($txt) + { + if (empty($this->order['processid'])) return; + $file_name = app()->getRuntimePath().'log/'.$this->order['processid'].'.log'; + file_put_contents($file_name, $txt . PHP_EOL, FILE_APPEND); + if(php_sapi_name() == 'cli'){ + echo $txt . PHP_EOL; + } + } +} diff --git a/app/service/CertTaskService.php b/app/service/CertTaskService.php new file mode 100644 index 0000000..83741f0 --- /dev/null +++ b/app/service/CertTaskService.php @@ -0,0 +1,92 @@ +execute_deploy(); + $this->execute_order(); + echo 'done'.PHP_EOL; + } + + private function execute_order() + { + $days = config_get('cert_renewdays', 7); + $list = Db::name('cert_order')->field('id,status,issend')->whereRaw('status NOT IN (3,4) AND (retrytime IS NULL OR retrytime date('Y-m-d H:i:s', time() + $days * 86400)])->select(); + //print_r($list);exit; + $failcount = 0; + foreach ($list as $row) { + try { + $service = new CertOrderService($row['id']); + if ($row['status'] == 3) { + $service->reset(); + } + $retcode = $service->process(); + if ($retcode == 3) { + echo 'ID:'.$row['id'].' 证书已签发成功!'.PHP_EOL; + if($row['issend'] == 0) MsgNotice::cert_send($row['id'], true); + } elseif ($retcode == 1) { + echo 'ID:'.$row['id'].' 添加DNS记录成功!'.PHP_EOL; + } + break; + } catch (Exception $e) { + echo 'ID:'.$row['id'].' '.$e->getMessage().PHP_EOL; + if ($e->getCode() == 102) { + break; + } elseif ($e->getCode() == 103) { + if($row['issend'] == 0) MsgNotice::cert_send($row['id'], false); + } else { + $failcount++; + } + } + if ($failcount >= 3) break; + sleep(1); + } + } + + private function execute_deploy() + { + $start = config_get('deploy_hour_start', 0); + $end = config_get('deploy_hour_end', 23); + $hour = date('H'); + if($start <= $end){ + if($hour < $start || $hour > $end){ + echo '不在部署任务运行时间范围内'.PHP_EOL; return; + } + }else{ + if($hour < $start && $hour > $end){ + echo '不在部署任务运行时间范围内'.PHP_EOL; return; + } + } + + $list = Db::name('cert_deploy')->field('id,status,issend')->whereRaw('status IN (0,-1) AND (retrytime IS NULL OR retrytimeselect(); + //print_r($list);exit; + $count = 0; + foreach ($list as $row) { + try { + $service = new CertDeployService($row['id']); + $service->process(); + echo 'ID:'.$row['id'].' 部署任务执行成功!'.PHP_EOL; + $count++; + } catch (Exception $e) { + echo 'ID:'.$row['id'].' '.$e->getMessage().PHP_EOL; + if ($e->getCode() == 102) { + break; + } elseif ($e->getCode() == 103) { + if($row['issend'] == 0) MsgNotice::cert_send($row['id'], false); + } else { + $count++; + } + } + if ($count >= 3) break; + sleep(1); + } + } +} diff --git a/app/lib/OptimizeService.php b/app/service/OptimizeService.php similarity index 96% rename from app/lib/OptimizeService.php rename to app/service/OptimizeService.php index 964629b..41a8678 100644 --- a/app/lib/OptimizeService.php +++ b/app/service/OptimizeService.php @@ -1,10 +1,14 @@ $list) { + $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.name', $mainDomain)->field('A.*,B.type')->find(); + if (!$drow) { + if ($cname) { + foreach ($list as $key => $row) { + if ($row['name'] == '_acme-challenge') { + $domain = $mainDomain; + } else { + $domain = str_replace('_acme-challenge.', '', $row['name']) . '.' . $mainDomain; + } + $cname_row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.domain', $domain)->field('A.*,B.name cnamedomain')->find(); + if ($cname_row) { + $row['name'] = $cname_row['rr']; + $cnameDomainList[$cname_row['cnamedomain']][] = $row; + unset($list[$key]); + } else { + throw new Exception('域名'.$domain.'未在本系统添加'); + } + } + } else { + throw new Exception('域名'.$mainDomain.'未在本系统添加'); + } + } + if (empty($list)) continue; + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + usort($list, function ($a, $b) { + return strcmp($a['name'], $b['name']); + }); + $records = []; + foreach ($list as $row) { + $domain = $row['name'] . '.' . $mainDomain; + if (!isset($records[$row['name']])) $records[$row['name']] = $dns->getSubDomainRecords($row['name'], 1, 100); + if (!$records[$row['name']]) throw new Exception('获取'.$domain.'记录列表失败,'.$dns->getError()); + + $filter_records = array_filter($records[$row['name']]['list'], function ($v) use ($row) { + return $v['Type'] == $row['type'] && $v['Value'] == $row['value']; + }); + if (!empty($filter_records)) { + foreach ($filter_records as $recordid => $record) { + unset($records[$row['name']]['list'][$recordid]); + } + continue; + } + + $filter_records = array_filter($records[$row['name']]['list'], function ($v) use ($row) { + return $v['Type'] == $row['type']; + }); + if (!empty($filter_records)) { + foreach ($filter_records as $recordid => $record) { + $dns->deleteDomainRecord($record['RecordId']); + unset($records[$row['name']]['list'][$recordid]); + $log('Delete DNS Record: '.$domain.' '.$row['type']); + } + } + + $res = $dns->addDomainRecord($row['name'], $row['type'], $row['value'], DnsHelper::$line_name[$drow['type']]['DEF'], 600); + if (!$res && $row['type'] != 'CAA') throw new Exception('添加'.$domain.'解析记录失败,' . $dns->getError()); + $log('Add DNS Record: '.$domain.' '.$row['type'].' '.$row['value']); + } + } + if (!empty($cnameDomainList)) { + self::addDns($cnameDomainList, $log); + } + } + + public static function delDns($dnsList, callable $log, $cname = false) + { + $cnameDomainList = []; + foreach ($dnsList as $mainDomain => $list) { + $drow = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')->where('A.name', $mainDomain)->field('A.*,B.type')->find(); + if (!$drow) { + if ($cname) { + foreach ($list as $key => $row) { + if ($row['name'] == '_acme-challenge') { + $domain = $mainDomain; + } else { + $domain = str_replace('_acme-challenge.', '', $row['name']) . '.' . $mainDomain; + } + $cname_row = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id')->where('A.domain', $domain)->field('A.*,B.name cnamedomain')->find(); + if ($cname_row) { + $row['name'] = $cname_row['rr']; + $cnameDomainList[$cname_row['cnamedomain']][] = $row; + unset($list[$key]); + } else { + throw new Exception('域名'.$domain.'未在本系统添加'); + } + } + } else { + throw new Exception('域名'.$mainDomain.'未在本系统添加'); + } + } + if (empty($list)) continue; + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + usort($list, function ($a, $b) { + return strcmp($a['name'], $b['name']); + }); + $records = []; + foreach ($list as $row) { + //if ($row['type'] == 'CAA') continue; + $domain = $row['name'] . '.' . $mainDomain; + if (!isset($records[$row['name']])) $records[$row['name']] = $dns->getSubDomainRecords($row['name'], 1, 100); + if (!$records[$row['name']]) throw new Exception('获取'.$domain.'记录列表失败,'.$dns->getError()); + + $filter_records = array_filter($records[$row['name']]['list'], function ($v) use ($row) { + return $v['Type'] == $row['type'] && ($v['Value'] == $row['value'] || rtrim($v['Value'], '.') == $row['value']); + }); + if (empty($filter_records)) continue; + + foreach ($filter_records as $record) { + $dns->deleteDomainRecord($record['RecordId']); + $log('Delete DNS Record: '.$domain.' '.$row['type'].' '.$row['value']); + } + } + } + if (!empty($cnameDomainList)) { + self::delDns($cnameDomainList, $log); + } + } + + public static function verifyDns($dnsList) + { + if (empty($dnsList)) return true; + foreach ($dnsList as $mainDomain => $list) { + foreach ($list as $row) { + if ($row['type'] == 'CAA') continue; + $domain = $row['name'] . '.' . $mainDomain; + $result = DnsQueryUtils::get_dns_records($domain, $row['type']); + if (!$result || !in_array($row['value'], $result) && !in_array(strtolower($row['value']), $result)) { + $result = DnsQueryUtils::query_dns_doh($domain, $row['type']); + if (!$result || !in_array($row['value'], $result) && !in_array(strtolower($row['value']), $result)) { + return false; + } + } + } + } + return true; + } +} diff --git a/app/lib/CheckUtils.php b/app/utils/CheckUtils.php similarity index 97% rename from app/lib/CheckUtils.php rename to app/utils/CheckUtils.php index d35b52e..ea45536 100644 --- a/app/lib/CheckUtils.php +++ b/app/utils/CheckUtils.php @@ -1,6 +1,6 @@ DNS_A, 'AAAA' => DNS_AAAA, 'CNAME' => DNS_CNAME, 'MX' => DNS_MX, 'TXT' => DNS_TXT]; + if (!array_key_exists($type, $dns_type)) return false; + $list = dns_get_record($domain, $dns_type[$type]); + if (!$list || empty($list)) return false; + $result = []; + foreach ($list as $row) { + if ($row['type'] == 'A') { + $result[] = $row['ip']; + } elseif ($row['type'] == 'AAAA') { + $result[] = $row['ipv6']; + } elseif ($row['type'] == 'CNAME') { + $result[] = $row['target']; + } elseif ($row['type'] == 'MX') { + $result[] = $row['target']; + } elseif ($row['type'] == 'TXT') { + $result[] = $row['txt']; + } + } + return $result; + } + + public static function query_dns_doh($domain, $type) + { + $dns_type = ['A' => 1, 'AAAA' => 28, 'CNAME' => 5, 'MX' => 15, 'TXT' => 16, 'SOA' => 6, 'NS' => 2, 'PTR' => 12, 'SRV' => 33, 'CAA' => 257]; + if (!array_key_exists($type, $dns_type)) return false; + $id = array_rand(self::$doh_servers); + $url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type]; + $data = get_curl($url); + $arr = json_decode($data, true); + if (!$arr) { + unset($doh_servers[$id]); + $id = array_rand(self::$doh_servers); + $url = self::$doh_servers[$id].'?name='.urlencode($domain).'&type='.$dns_type[$type]; + $data = get_curl($url); + $arr = json_decode($data, true); + if (!$arr) return false; + } + $result = []; + if (isset($arr['Answer'])) { + foreach ($arr['Answer'] as $row) { + $value = $row['data']; + if ($row['type'] == 5) $value = trim($value, '.'); + if ($row['type'] == 16) $value = trim($value, '"'); + $result[] = $value; + } + } + return $result; + } +} diff --git a/app/lib/MsgNotice.php b/app/utils/MsgNotice.php similarity index 57% rename from app/lib/MsgNotice.php rename to app/utils/MsgNotice.php index 7d54b8f..5fb8e6c 100644 --- a/app/lib/MsgNotice.php +++ b/app/utils/MsgNotice.php @@ -1,6 +1,9 @@ field('id,aid,issuetime,expiretime,issuer,status,error')->where('id', $id)->find(); + if (!$row) return; + $type = Db::name('cert_account')->where('id', $row['aid'])->value('type'); + $domainList = Db::name('cert_domain')->where('oid', $id)->column('domain'); + if (empty($domainList)) return; + if ($result) { + if (count($domainList) > 1) { + $mail_title = $domainList[0] . '等' . count($domainList) . '个域名SSL证书签发成功通知'; + } else { + $mail_title = $domainList[0] . '域名SSL证书签发成功通知'; + } + $mail_content = '尊敬的用户,您好:您的SSL证书已签发成功!
证书账户:'.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')
证书域名:'.implode('、', $domainList).'
签发时间:'.$row['issuetime'].'
到期时间:'.$row['expiretime'].'
颁发机构:'.$row['issuer']; + } else { + $status_arr = [0 => '失败', -1 => '购买证书失败', -2 => '创建订单失败', -3 => '添加DNS失败', -4 => '验证DNS失败', -5 => '验证订单失败', -6 => '订单验证未通过', -7 => '签发证书失败']; + if(count($domainList) > 1){ + $mail_title = $domainList[0].'等'.count($domainList).'个域名SSL证书'.$status_arr[$row['status']].'通知'; + }else{ + $mail_title = $domainList[0].'域名SSL证书'.$status_arr[$row['status']].'通知'; + } + $mail_content = '尊敬的用户,您好:您的SSL证书'.$status_arr[$row['status']].'!
证书账户:'.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')
证书域名:'.implode('、', $domainList).'
失败时间:'.date('Y-m-d H:i:s').'
失败原因:'.$row['error']; + } + $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s'); + + if (config_get('cert_notice_mail') == 1 || config_get('cert_notice_mail') == 2 && !$result) { + $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); + self::send_mail($mail_name, $mail_title, $mail_content); + } + if (config_get('cert_notice_wxtpl') == 1 || config_get('cert_notice_wxtpl') == 2 && !$result) { + $content = str_replace(['
', '', ''], ["\n\n", '**', '**'], $mail_content); + self::send_wechat_tplmsg($mail_title, $content); + } + if (config_get('cert_notice_tgbot') == 1 || config_get('cert_notice_tgbot') == 2 && !$result) { + $content = str_replace('
', "\n", $mail_content); + $content = "" . $mail_title . "\n" . $content; + self::send_telegram_bot($content); + } + Db::name('cert_order')->where('id', $id)->update(['issend' => 1]); + } + + public static function deploy_send($id, $result) + { + $row = Db::name('cert_deploy')->field('id,aid,oid,remark,status,error')->where('id', $id)->find(); + if (!$row) return; + $account = Db::name('cert_account')->field('id,type,name,remark')->where('id', $row['aid'])->find(); + $domainList = Db::name('cert_domain')->where('oid', $row['oid'])->column('domain'); + $typename = DeployHelper::$deploy_config[$account['type']]['name']; + $mail_title = $typename; + if(!empty($row['remark'])) $mail_title .= '('.$row['remark'].')'; + $mail_title .= '证书部署'.($result?'成功':'失败').'通知'; + if ($result) { + $mail_content = '尊敬的用户,您好:您的SSL证书已成功部署到'.$typename.'!
自动部署账户:['.$account['id'].']'.$typename.'('.($account['remark']?$account['remark']:$account['name']).')
关联SSL证书:['.$row['oid'].']'.implode('、', $domainList).'
任务备注:'.($row['remark']?$row['remark']:'无'); + } else { + $mail_content = '尊敬的用户,您好:您的SSL证书部署失败!
失败原因:'.$row['error'].'
自动部署账户:['.$account['id'].']'.$typename.'('.($account['remark']?$account['remark']:$account['name']).')
关联SSL证书:['.$row['oid'].']'.implode('、', $domainList).'
任务备注:'.($row['remark']?$row['remark']:'无'); + } + $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s'); + + if (config_get('cert_notice_mail') == 1 || config_get('cert_notice_mail') == 2 && !$result) { + $mail_name = config_get('mail_recv') ? config_get('mail_recv') : config_get('mail_name'); + self::send_mail($mail_name, $mail_title, $mail_content); + } + if (config_get('cert_notice_wxtpl') == 1 || config_get('cert_notice_wxtpl') == 2 && !$result) { + $content = str_replace(['
', '', ''], ["\n\n", '**', '**'], $mail_content); + self::send_wechat_tplmsg($mail_title, $content); + } + if (config_get('cert_notice_tgbot') == 1 || config_get('cert_notice_tgbot') == 2 && !$result) { + $content = str_replace('
', "\n", $mail_content); + $content = "" . $mail_title . "\n" . $content; + self::send_telegram_bot($content); + } + Db::name('cert_deploy')->where('id', $id)->update(['issend' => 1]); + } + public static function send_mail($to, $sub, $msg) { $mail_type = config_get('mail_type'); diff --git a/app/view/auth/login.html b/app/view/auth/login.html index 281b9bc..947675b 100644 --- a/app/view/auth/login.html +++ b/app/view/auth/login.html @@ -30,6 +30,9 @@ a{color:#444} #login-form{margin-top:20px} #login-form .input-group{margin-bottom:15px} #login-form .form-control{font-size:14px} +#totp-form{margin-top:20px} +#totp-form .input-group{margin-bottom:15px} +#totp-form .form-control{font-size:14px} @@ -56,18 +59,49 @@ a{color:#444}
- +
+ + + + + \ No newline at end of file diff --git a/app/view/cert/account_form.html b/app/view/cert/account_form.html new file mode 100644 index 0000000..764b0c1 --- /dev/null +++ b/app/view/cert/account_form.html @@ -0,0 +1,237 @@ +{extend name="common/layout" /} +{block name="title"}{$title}{/block} +{block name="main"} + +
+
+
+

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

+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+ + 提示: +
+
+
+
+
+
+
+
+
+{/block} +{block name="script"} + + + + +{/block} \ No newline at end of file diff --git a/app/view/cert/certaccount.html b/app/view/cert/certaccount.html new file mode 100644 index 0000000..dcfb725 --- /dev/null +++ b/app/view/cert/certaccount.html @@ -0,0 +1,93 @@ +{extend name="common/layout" /} +{block name="title"}SSL证书账户管理{/block} +{block name="main"} +
+
+
+
+ +
+
+ + +
+ + 刷新 + 添加 +
+ + +
+
+
+
+
+{/block} +{block name="script"} + + + + + +{/block} \ No newline at end of file diff --git a/app/view/cert/certorder.html b/app/view/cert/certorder.html new file mode 100644 index 0000000..c3239b4 --- /dev/null +++ b/app/view/cert/certorder.html @@ -0,0 +1,385 @@ +{extend name="common/layout" /} +{block name="title"}SSL证书订单列表{/block} +{block name="main"} + +
+
+
+
+ +
+ +
+ +
+ +
+
+
+ +
+ + 刷新 + 添加 +
+ + +
+
+
+
+
+{/block} +{block name="script"} + + + + + + +{/block} \ No newline at end of file diff --git a/app/view/cert/certset.html b/app/view/cert/certset.html new file mode 100644 index 0000000..a7fa391 --- /dev/null +++ b/app/view/cert/certset.html @@ -0,0 +1,115 @@ +{extend name="common/layout" /} +{block name="title"}SSL证书计划任务{/block} +{block name="main"} +
+
+
+

计划任务说明

+
+

  • 计划任务:将以下命令添加到计划任务,1分钟1次
  • +

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

    +
    +
    + +
    +

    自动续签设置

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

    自动部署设置

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

    通知设置

    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +{/block} +{block name="script"} + + +{/block} \ No newline at end of file diff --git a/app/view/cert/cname.html b/app/view/cert/cname.html new file mode 100644 index 0000000..e7867e6 --- /dev/null +++ b/app/view/cert/cname.html @@ -0,0 +1,244 @@ +{extend name="common/layout" /} +{block name="title"}CMAME代理记录管理{/block} +{block name="main"} + + +

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

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

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

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

    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + + 提示: +
    +
    +
    +
    +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + + +{/block} \ No newline at end of file diff --git a/app/view/cert/deployaccount.html b/app/view/cert/deployaccount.html new file mode 100644 index 0000000..a392d80 --- /dev/null +++ b/app/view/cert/deployaccount.html @@ -0,0 +1,93 @@ +{extend name="common/layout" /} +{block name="title"}自动部署任务账户管理{/block} +{block name="main"} +
    +
    +
    +
    + +
    +
    + + +
    + + 刷新 + 添加 +
    + + +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + +{/block} \ No newline at end of file diff --git a/app/view/cert/deploytask.html b/app/view/cert/deploytask.html new file mode 100644 index 0000000..fe07dca --- /dev/null +++ b/app/view/cert/deploytask.html @@ -0,0 +1,286 @@ +{extend name="common/layout" /} +{block name="title"}SSL证书自动部署任务{/block} +{block name="main"} + +
    +
    +
    +
    + +
    + +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + + 刷新 + 添加 +
    + + +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + + +{/block} \ No newline at end of file diff --git a/app/view/cert/order_form.html b/app/view/cert/order_form.html new file mode 100644 index 0000000..b8e9102 --- /dev/null +++ b/app/view/cert/order_form.html @@ -0,0 +1,157 @@ +{extend name="common/layout" /} +{block name="title"}SSL证书订单{/block} +{block name="main"} + +
    +
    +
    +

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

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

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

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

    +
    +
    +
    +{/block} +{block name="script"} + + + + +{/block} \ No newline at end of file diff --git a/app/view/common/layout.html b/app/view/common/layout.html index 40cdc15..2d2bea0 100644 --- a/app/view/common/layout.html +++ b/app/view/common/layout.html @@ -11,7 +11,7 @@ - + @@ -202,8 +226,8 @@
    - - + + {/block} \ No newline at end of file diff --git a/app/view/dmonitor/task.html b/app/view/dmonitor/task.html index 5cdcddd..34efc01 100644 --- a/app/view/dmonitor/task.html +++ b/app/view/dmonitor/task.html @@ -57,7 +57,7 @@ $(document).ready(function(){ field: 'rr', title: '域名', formatter: function(value, row, index) { - return '' + value + '.' + row.domain + ''; + return '' + value + '.' + row.domain + ''; } }, { @@ -114,13 +114,11 @@ $(document).ready(function(){ field: 'active', title: '运行开关', formatter: function(value, row, index) { - var html = ''; - if(value == 1) { - html += '开启'; - } else { - html += '暂停'; + if(value == 1){ + return '
    '; + }else{ + return '
    '; } - return html; } }, { diff --git a/app/view/index/setpwd.html b/app/view/index/setpwd.html index 90d1601..9b4f198 100644 --- a/app/view/index/setpwd.html +++ b/app/view/index/setpwd.html @@ -25,10 +25,58 @@
    +
    +

    TOTP二次验证

    +
    +
    +
    +
    + {if $user.totp_open == 1} + +
    +
    + {else} + +
    + {/if} +
    +
    +
    +
    + +
    + {/block} {block name="script"} + + {/block} \ No newline at end of file diff --git a/app/view/optimizeip/opiplist.html b/app/view/optimizeip/opiplist.html index a633597..8a283e3 100644 --- a/app/view/optimizeip/opiplist.html +++ b/app/view/optimizeip/opiplist.html @@ -57,7 +57,7 @@ $(document).ready(function(){ field: 'rr', title: '域名', formatter: function(value, row, index) { - return '' + value + '.' + row.domain + ''; + return '' + value + '.' + row.domain + ''; } }, { @@ -98,13 +98,11 @@ $(document).ready(function(){ field: 'active', title: '任务开关', formatter: function(value, row, index) { - var html = ''; - if(value == 1) { - html += '开启'; - } else { - html += '关闭'; + if(value == 1){ + return '
    '; + }else{ + return '
    '; } - return html; } }, { diff --git a/app/view/system/noticeset.html b/app/view/system/noticeset.html new file mode 100644 index 0000000..a71a710 --- /dev/null +++ b/app/view/system/noticeset.html @@ -0,0 +1,200 @@ +{extend name="common/layout" /} +{block name="title"}通知设置{/block} +{block name="main"} +
    +
    +
    +

    发信邮箱设置

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

    微信公众号消息接口设置

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

    Telegram机器人接口设置

    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +{/block} +{block name="script"} + + +{/block} \ No newline at end of file diff --git a/app/view/system/proxyset.html b/app/view/system/proxyset.html new file mode 100644 index 0000000..f063f13 --- /dev/null +++ b/app/view/system/proxyset.html @@ -0,0 +1,113 @@ +{extend name="common/layout" /} +{block name="title"}容灾切换代理设置{/block} +{block name="main"} +
    +
    +
    +

    代理服务器设置

    +
    +
    +
    + +
    +

    +
    + +
    +

    +
    + +
    +

    +
    + +
    +

    +
    + +
    +

    + +
    +
    + +
    +
    +
    +{/block} +{block name="script"} + + +{/block} \ No newline at end of file diff --git a/config/app.php b/config/app.php index 87ae3bc..e6cd05a 100644 --- a/config/app.php +++ b/config/app.php @@ -31,7 +31,7 @@ return [ 'show_error_msg' => true, 'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl', - 'version' => '1018', + 'version' => '1021', - 'dbversion' => '1011' + 'dbversion' => '1021' ]; diff --git a/config/console.php b/config/console.php index ebb1915..2cf352c 100644 --- a/config/console.php +++ b/config/console.php @@ -7,5 +7,7 @@ return [ 'commands' => [ 'dmtask' => 'app\command\Dmtask', 'opiptask' => 'app\command\Opiptask', + 'certtask' => 'app\command\Certtask', + 'reset' => 'app\command\Reset', ], ]; diff --git a/public/static/css/bootstrap-table.css b/public/static/css/bootstrap-table.css index f7beca8..607d063 100644 --- a/public/static/css/bootstrap-table.css +++ b/public/static/css/bootstrap-table.css @@ -5,3 +5,9 @@ .form-inline .form-control{display:inline-block;width:auto;vertical-align:middle} .form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle} .bv-form .help-block{margin-bottom:0}.bv-form .tooltip-inner{text-align:left}.nav-tabs li.bv-tab-success>a{color:#3c763d}.nav-tabs li.bv-tab-error>a{color:#a94442}.bv-form .bv-icon-no-label{top:0}.bv-form .bv-icon-input-group{top:0;z-index:100} +.material-switch>input[type=checkbox]{display:none} +.material-switch>label{cursor:pointer;height:0;position:relative;width:36px;margin-left:4px} +.material-switch>label::before{background:#000;box-shadow:inset 0 0 10px rgba(0,0,0,.5);border-radius:8px;content:'';height:16px;margin-top:-7px;position:absolute;opacity:.3;transition:all .4s ease-in-out;width:36px} +.material-switch>label::after{background:#fff;border-radius:16px;box-shadow:0 0 5px rgba(0,0,0,.3);content:'';height:20px;left:-2px;margin-top:-5px;position:absolute;top:-4px;transition:all .3s ease-in-out;width:20px} +.material-switch>input[type=checkbox]:checked+label::before{background:inherit;opacity:.5} +.material-switch>input[type=checkbox]:checked+label::after{background:inherit;left:20px} diff --git a/public/static/images/aws.ico b/public/static/images/aws.ico new file mode 100644 index 0000000..643489f Binary files /dev/null and b/public/static/images/aws.ico differ diff --git a/public/static/images/bt.ico b/public/static/images/bt.ico new file mode 100644 index 0000000..0a5d0ea Binary files /dev/null and b/public/static/images/bt.ico differ diff --git a/public/static/images/cloud.png b/public/static/images/cloud.png new file mode 100644 index 0000000..9babf4d Binary files /dev/null and b/public/static/images/cloud.png differ diff --git a/public/static/images/gcore.ico b/public/static/images/gcore.ico new file mode 100644 index 0000000..652db76 Binary files /dev/null and b/public/static/images/gcore.ico differ diff --git a/public/static/images/google.ico b/public/static/images/google.ico new file mode 100644 index 0000000..82339b3 Binary files /dev/null and b/public/static/images/google.ico differ diff --git a/public/static/images/host.png b/public/static/images/host.png new file mode 100644 index 0000000..5a17b1d Binary files /dev/null and b/public/static/images/host.png differ diff --git a/public/static/images/letsencrypt.ico b/public/static/images/letsencrypt.ico new file mode 100644 index 0000000..9196d22 Binary files /dev/null and b/public/static/images/letsencrypt.ico differ diff --git a/public/static/images/opanel.png b/public/static/images/opanel.png new file mode 100644 index 0000000..6f82a12 Binary files /dev/null and b/public/static/images/opanel.png differ diff --git a/public/static/images/qiniu.ico b/public/static/images/qiniu.ico new file mode 100644 index 0000000..887a71b Binary files /dev/null and b/public/static/images/qiniu.ico differ diff --git a/public/static/images/safeline.png b/public/static/images/safeline.png new file mode 100644 index 0000000..d0797d7 Binary files /dev/null and b/public/static/images/safeline.png differ diff --git a/public/static/images/server.png b/public/static/images/server.png new file mode 100644 index 0000000..4ef6196 Binary files /dev/null and b/public/static/images/server.png differ diff --git a/public/static/images/server2.png b/public/static/images/server2.png new file mode 100644 index 0000000..478f124 Binary files /dev/null and b/public/static/images/server2.png differ diff --git a/public/static/images/ssl.ico b/public/static/images/ssl.ico new file mode 100644 index 0000000..d83c3ae Binary files /dev/null and b/public/static/images/ssl.ico differ diff --git a/public/static/images/tencent.ico b/public/static/images/tencent.ico new file mode 100644 index 0000000..1e94a78 Binary files /dev/null and b/public/static/images/tencent.ico differ diff --git a/public/static/images/ucloud.ico b/public/static/images/ucloud.ico new file mode 100644 index 0000000..570e650 Binary files /dev/null and b/public/static/images/ucloud.ico differ diff --git a/public/static/images/waf.png b/public/static/images/waf.png new file mode 100644 index 0000000..6c65f2a Binary files /dev/null and b/public/static/images/waf.png differ diff --git a/public/static/images/zerossl.ico b/public/static/images/zerossl.ico new file mode 100644 index 0000000..5fd2a3d Binary files /dev/null and b/public/static/images/zerossl.ico differ diff --git a/public/static/js/custom.js b/public/static/js/custom.js index 228c852..f4fc0bf 100644 --- a/public/static/js/custom.js +++ b/public/static/js/custom.js @@ -79,7 +79,7 @@ if (typeof $.fn.bootstrapTable !== "undefined") { queryParamsType: '', queryParams: function(params) { $('#searchToolbar').find(':input[name]').each(function() { - if(!$(this).is(":visible")) return; + //if(!$(this).is(":visible")) return; params[$(this).attr('name')] = $(this).val() }) updateQueryStr(params); diff --git a/route/app.php b/route/app.php index 2a6d686..e9466e1 100644 --- a/route/app.php +++ b/route/app.php @@ -21,10 +21,10 @@ Route::pattern([ Route::any('/install', 'install/index') ->middleware(ViewOutput::class); -Route::get('/verifycode', 'auth/verifycode')->middleware(SessionInit::class) -->middleware(ViewOutput::class); Route::any('/login', 'auth/login')->middleware(SessionInit::class) ->middleware(ViewOutput::class); +Route::get('/verifycode', 'auth/verifycode')->middleware(SessionInit::class); +Route::post('/auth/totp', 'auth/totp')->middleware(SessionInit::class); Route::get('/logout', 'auth/logout'); Route::any('/quicklogin', 'auth/quicklogin'); Route::any('/dmtask/status', 'dmonitor/status'); @@ -35,6 +35,7 @@ Route::group(function () { Route::post('/changeskin', 'index/changeskin'); Route::get('/cleancache', 'index/cleancache'); Route::any('/setpwd', 'index/setpwd'); + Route::any('/totp/:action', 'index/totp'); Route::get('/test', 'index/test'); Route::post('/user/data', 'user/user_data'); @@ -72,11 +73,6 @@ Route::group(function () { Route::get('/dmonitor/task/info/:id', 'dmonitor/taskinfo'); Route::any('/dmonitor/task/:action', 'dmonitor/taskform'); Route::get('/dmonitor/task', 'dmonitor/task'); - Route::any('/dmonitor/noticeset', 'dmonitor/noticeset'); - Route::any('/dmonitor/proxyset', 'dmonitor/proxyset'); - Route::get('/dmonitor/mailtest', 'dmonitor/mailtest'); - Route::get('/dmonitor/tgbottest', 'dmonitor/tgbottest'); - Route::post('/dmonitor/proxytest', 'dmonitor/proxytest'); Route::post('/dmonitor/clean', 'dmonitor/clean'); Route::any('/optimizeip/opipset', 'optimizeip/opipset'); @@ -85,6 +81,36 @@ Route::group(function () { Route::get('/optimizeip/opiplist', 'optimizeip/opiplist'); Route::any('/optimizeip/opipform/:action', 'optimizeip/opipform'); + Route::get('/cert/certaccount', 'cert/certaccount'); + Route::get('/cert/deployaccount', 'cert/deployaccount'); + Route::post('/cert/account/data', 'cert/account_data'); + Route::post('/cert/account/:action', 'cert/account_op'); + Route::get('/cert/account/:action', 'cert/account_form'); + + Route::get('/cert/certorder', 'cert/certorder'); + Route::post('/cert/order/data', 'cert/order_data'); + Route::post('/cert/order/process', 'cert/order_process'); + Route::post('/cert/order/:action', 'cert/order_op'); + Route::get('/cert/order/:action', 'cert/order_form'); + + Route::get('/cert/deploytask', 'cert/deploytask'); + Route::post('/cert/deploy/data', 'cert/deploy_data'); + Route::post('/cert/deploy/process', 'cert/deploy_process'); + Route::post('/cert/deploy/:action', 'cert/deploy_op'); + Route::get('/cert/deploy/:action', 'cert/deploy_form'); + + Route::get('/cert/cname', 'cert/cname'); + Route::post('/cert/cname/data', 'cert/cname_data'); + Route::post('/cert/cname/:action', 'cert/cname_op'); + + Route::any('/cert/certset', 'cert/certset'); + + Route::any('/system/noticeset', 'system/noticeset'); + Route::any('/system/proxyset', 'system/proxyset'); + Route::get('/system/mailtest', 'system/mailtest'); + Route::get('/system/tgbottest', 'system/tgbottest'); + Route::post('/system/proxytest', 'system/proxytest'); + })->middleware(CheckLogin::class) ->middleware(ViewOutput::class);