SSL证书申请与部署

This commit is contained in:
net909 2024-12-21 17:07:51 +08:00
parent 1ed93cd295
commit b585e5fa55
119 changed files with 15923 additions and 806 deletions

View File

@ -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)
### 其他推荐

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

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

View File

@ -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
{

View File

@ -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
{

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

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace app\command;
use Exception;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Config;
class Reset extends Command
{
protected function configure()
{
// 指令配置
$this->setName('reset')
->addArgument('type', Argument::REQUIRED, '操作类型,pwd:重置密码,totp:关闭TOTP')
->addArgument('username', Argument::REQUIRED, '用户名')
->addArgument('password', Argument::OPTIONAL, '密码')
->setDescription('重置密码');
}
protected function execute(Input $input, Output $output)
{
$type = trim($input->getArgument('type'));
$username = trim($input->getArgument('username'));
$user = Db::name('user')->where('username', $username)->find();
if (!$user) {
$output->writeln('用户 ' . $username . ' 不存在');
return;
}
if ($type == 'pwd') {
$password = $input->getArgument('password');
if (empty($password)) $password = '123456';
Db::name('user')->where('id', $user['id'])->update(['password' => password_hash($password, PASSWORD_DEFAULT)]);
$output->writeln('用户 ' . $username . ' 密码重置成功');
} elseif ($type == 'totp') {
Db::name('user')->where('id', $user['id'])->update(['totp_open' => 0, 'totp_secret' => null]);
$output->writeln('用户 ' . $username . ' TOTP关闭成功');
}
}
}

View File

@ -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()

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

@ -0,0 +1,735 @@
<?php
namespace app\controller;
use app\BaseController;
use app\lib\CertHelper;
use app\lib\DeployHelper;
use app\service\CertOrderService;
use app\service\CertDeployService;
use Exception;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
class Cert extends BaseController
{
public function certaccount()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
return view();
}
public function deployaccount()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
return view();
}
public function account_data()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$deploy = input('get.deploy/d', 0);
$kw = $this->request->post('kw', null, 'trim');
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$select = Db::name('cert_account')->where('deploy', $deploy);
if (!empty($kw)) {
$select->whereLike('name|remark', '%' . $kw . '%');
}
$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();
}
}

View File

@ -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, '邮件发送测试。', '这是一封测试邮件!<br/><br/>来自:' . $this->request->root(true));
if ($result === true) {
return json(['code' => 0, 'msg' => '邮件发送成功!']);
} else {
return json(['code' => -1, 'msg' => '邮件发送失败!' . $result]);
}
}
public function tgbottest()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$tgbot_token = config_get('tgbot_token');
$tgbot_chatid = config_get('tgbot_chatid');
if (empty($tgbot_token) || empty($tgbot_chatid)) return json(['code' => -1, 'msg' => '请先保存设置']);
$content = "<strong>消息发送测试</strong>\n\n这是一封测试消息!\n\n来自:" . $this->request->root(true);
$result = \app\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()

View File

@ -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;
});
}
}

View File

@ -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
{

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

@ -0,0 +1,98 @@
<?php
namespace app\controller;
use app\BaseController;
use Exception;
use think\facade\Db;
use think\facade\View;
use think\facade\Cache;
class System 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']);
}
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, '邮件发送测试。', '这是一封测试邮件!<br/><br/>来自:' . $this->request->root(true));
if ($result === true) {
return json(['code' => 0, 'msg' => '邮件发送成功!']);
} else {
return json(['code' => -1, 'msg' => '邮件发送失败!' . $result]);
}
}
public function tgbottest()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$tgbot_token = config_get('tgbot_token');
$tgbot_chatid = config_get('tgbot_chatid');
if (empty($tgbot_token) || empty($tgbot_chatid)) return json(['code' => -1, 'msg' => '请先保存设置']);
$content = "<strong>消息发送测试</strong>\n\n这是一封测试消息!\n\n来自:" . $this->request->root(true);
$result = \app\utils\MsgNotice::send_telegram_bot($content);
if ($result === true) {
return json(['code' => 0, 'msg' => '消息发送成功!']);
} else {
return json(['code' => -1, 'msg' => '消息发送失败!' . $result]);
}
}
public function 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]);
}
}

1925
app/data/domain_root.txt Normal file

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,349 @@
<?php
namespace app\lib;
use think\facade\Db;
class CertHelper
{
public static $cert_config = [
'letsencrypt' => [
'name' => 'Let\'s Encrypt',
'class' => 1,
'icon' => 'letsencrypt.ico',
'wildcard' => true,
'max_domains' => 100,
'cname' => true,
'note' => null,
'inputs' => [
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => '用于注册Let\'s Encrypt账号',
'required' => true,
],
'mode' => [
'name' => '环境选择',
'type' => 'radio',
'options' => [
'live' => '正式环境',
'staging' => '测试环境',
],
'value' => 'live'
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
]
],
'zerossl' => [
'name' => 'ZeroSSL',
'class' => 1,
'icon' => 'zerossl.ico',
'wildcard' => true,
'max_domains' => 100,
'cname' => true,
'note' => '<a href="https://app.zerossl.com/developer" target="_blank" rel="noreferrer">ZeroSSL密钥生成地址</a>',
'inputs' => [
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => 'EAB申请邮箱',
'required' => true,
],
'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' => '<a href="https://cloud.google.com/certificate-manager/docs/public-ca-tutorial" target="_blank" rel="noreferrer">查看Google SSL账户配置说明</a>',
'inputs' => [
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => 'EAB申请邮箱',
'required' => true,
],
'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张免费证书额度证书到期或吊销可释放额度。<a href="https://cloud.tencent.com/document/product/400/89868" target="_blank" rel="noreferrer">腾讯云免费SSL简介与额度说明</a>',
'inputs' => [
'SecretId' => [
'name' => 'SecretId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'SecretKey' => [
'name' => 'SecretKey',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => '申请证书时填写的邮箱',
'required' => true,
],
]
],
'aliyun' => [
'name' => '阿里云免费SSL',
'class' => 2,
'icon' => 'aliyun.ico',
'wildcard' => false,
'max_domains' => 1,
'cname' => false,
'note' => '每个自然年有20张免费证书额度证书到期或吊销不释放额度。需要先进入阿里云控制台-<a href="https://yundun.console.aliyun.com/?p=cas#/certExtend/free/cn-hangzhou" target="_blank" rel="noreferrer">数字证书管理服务</a>,购买个人测试证书资源包。',
'inputs' => [
'AccessKeyId' => [
'name' => 'AccessKeyId',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'AccessKeySecret' => [
'name' => 'AccessKeySecret',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'username' => [
'name' => '姓名',
'type' => 'input',
'placeholder' => '申请联系人的姓名',
'required' => true,
],
'phone' => [
'name' => '手机号码',
'type' => 'input',
'placeholder' => '申请联系人的手机号码',
'required' => true,
],
'email' => [
'name' => '邮箱地址',
'type' => 'input',
'placeholder' => '申请联系人的邮箱地址',
'required' => true,
],
]
],
'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;
}
}

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

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

1327
app/lib/DeployHelper.php Normal file

File diff suppressed because it is too large Load Diff

View File

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

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

@ -0,0 +1,228 @@
<?php
namespace app\lib;
class TOTP
{
private static $BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
private $period = 30;
private $digest = 'sha1';
private $digits = 6;
private $epoch = 0;
private $secret;
private $issuer;
private $label;
public function __construct(?string $secret)
{
if ($secret == null) {
$secret = $this->generateSecret();
}
$this->secret = $secret;
}
public static function create(?string $secret = null)
{
return new self($secret);
}
public function getSecret(): string
{
return $this->secret;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): void
{
$this->label = $label;
}
public function getIssuer(): ?string
{
return $this->issuer;
}
public function setIssuer(string $issuer): void
{
$this->issuer = $issuer;
}
public function verify(string $otp, ?int $timestamp = null, ?int $window = null): bool
{
$timestamp = $this->getTimestamp($timestamp);
if (null === $window) {
return $this->compareOTP($this->at($timestamp), $otp);
}
return $this->verifyOtpWithWindow($otp, $timestamp, $window);
}
private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): bool
{
$window = abs($window);
for ($i = 0; $i <= $window; ++$i) {
$next = $i * $this->period + $timestamp;
$previous = -$i * $this->period + $timestamp;
$valid = $this->compareOTP($this->at($next), $otp) ||
$this->compareOTP($this->at($previous), $otp);
if ($valid) {
return true;
}
}
return false;
}
public function getProvisioningUri(): string
{
$params = [];
if (30 !== $this->period) {
$params['period'] = $this->period;
}
if (0 !== $this->epoch) {
$params['epoch'] = $this->epoch;
}
$label = $this->getLabel();
if (null === $label) {
throw new \InvalidArgumentException('The label is not set.');
}
if ($this->hasColon($label)) {
throw new \InvalidArgumentException('Label must not contain a colon.');
}
$params['issuer'] = $this->getIssuer();
$params['secret'] = $this->getSecret();
$query = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($params));
return sprintf('otpauth://totp/%s?%s', rawurlencode((null !== $this->getIssuer() ? $this->getIssuer() . ':' : '') . $label), $query);
}
/**
* The OTP at the specified input.
*/
private function generateOTP(int $input): string
{
$hash = hash_hmac($this->digest, $this->intToByteString($input), $this->base32_decode($this->getSecret()), true);
$hmac = array_values(unpack('C*', $hash));
$offset = ($hmac[\count($hmac) - 1] & 0xF);
$code = ($hmac[$offset + 0] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF);
$otp = $code % (10 ** $this->digits);
return str_pad((string) $otp, $this->digits, '0', STR_PAD_LEFT);
}
private function at(int $timestamp): string
{
return $this->generateOTP($this->timecode($timestamp));
}
private function timecode(int $timestamp): int
{
return (int) floor(($timestamp - $this->epoch) / $this->period);
}
private function getTimestamp(?int $timestamp): int
{
$timestamp = $timestamp ?? time();
if ($timestamp < 0) {
throw new \InvalidArgumentException('Timestamp must be at least 0.');
}
return $timestamp;
}
private function generateSecret(): string
{
return strtoupper($this->base32_encode(random_bytes(20)));
}
private function base32_encode($data)
{
$dataSize = strlen($data);
$res = '';
$remainder = 0;
$remainderSize = 0;
for ($i = 0; $i < $dataSize; $i++) {
$b = ord($data[$i]);
$remainder = ($remainder << 8) | $b;
$remainderSize += 8;
while ($remainderSize > 4) {
$remainderSize -= 5;
$c = $remainder & (31 << $remainderSize);
$c >>= $remainderSize;
$res .= self::$BASE32_ALPHABET[$c];
}
}
if ($remainderSize > 0) {
$remainder <<= (5 - $remainderSize);
$c = $remainder & 31;
$res .= self::$BASE32_ALPHABET[$c];
}
return $res;
}
private function base32_decode($data)
{
$data = strtolower($data);
$dataSize = strlen($data);
$buf = 0;
$bufSize = 0;
$res = '';
for ($i = 0; $i < $dataSize; $i++) {
$c = $data[$i];
$b = strpos(self::$BASE32_ALPHABET, $c);
if ($b === false) {
throw new \Exception('Encoded string is invalid, it contains unknown char #'.ord($c));
}
$buf = ($buf << 5) | $b;
$bufSize += 5;
if ($bufSize > 7) {
$bufSize -= 8;
$b = ($buf & (0xff << $bufSize)) >> $bufSize;
$res .= chr($b);
}
}
return $res;
}
private function intToByteString(int $int): string
{
$result = [];
while (0 !== $int) {
$result[] = \chr($int & 0xFF);
$int >>= 8;
}
return str_pad(implode(array_reverse($result)), 8, "\000", STR_PAD_LEFT);
}
private function compareOTP(string $safe, string $user): bool
{
return hash_equals($safe, $user);
}
private function hasColon(string $value): bool
{
$colons = [':', '%3A', '%3a'];
foreach ($colons as $colon) {
if (false !== mb_strpos($value, $colon)) {
return true;
}
}
return false;
}
}

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

@ -0,0 +1,683 @@
<?php
namespace app\lib\acme;
use Exception;
use stdClass;
/**
* ACMECert
* https://github.com/skoerfgen/ACMECert
*/
class ACMECert extends ACMEv2
{
private $alternate_chains = array();
public function register($termsOfServiceAgreed = false, $contacts = array())
{
return $this->_register($termsOfServiceAgreed, $contacts);
}
public function registerEAB($termsOfServiceAgreed, $eab_kid, $eab_hmac, $contacts = array())
{
if (!$this->resources) $this->readDirectory();
$protected = array(
'alg' => 'HS256',
'kid' => $eab_kid,
'url' => $this->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;
}));
}
}

View File

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

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

@ -0,0 +1,428 @@
<?php
namespace app\lib\acme;
use Exception;
class ACMEv2
{ // Communication with Let's Encrypt via ACME v2 protocol
protected
$ch = null, $logger = true, $bits, $sha_bits, $directory, $resources, $jwk_header, $kid_header, $account_key, $thumbprint, $nonce = null, $proxy;
private $delay_until = null;
public function __construct($directory, $proxy = false)
{
$this->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())
);
}
}

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

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

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

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

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

@ -0,0 +1,118 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\acme\ACMECert;
use Exception;
class google implements CertInterface
{
private $directories = array(
'live' => 'https://dv.acme-v02.api.pki.goog/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);
}
}

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

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

View File

@ -0,0 +1,113 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\acme\ACMECert;
use Exception;
class letsencrypt implements CertInterface
{
private $directories = array(
'live' => 'https://acme-v02.api.letsencrypt.org/directory',
'staging' => 'https://acme-staging-v02.api.letsencrypt.org/directory'
);
private $ac;
private $config;
private $ext;
public function __construct($config, $ext = null)
{
$this->config = $config;
if (empty($config['mode'])) $config['mode'] = 'live';
$this->ac = new ACMECert($this->directories[$config['mode']], $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);
}
}

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

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

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

@ -0,0 +1,187 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\client\Ucloud as UcloudClient;
use Exception;
class ucloud implements CertInterface
{
private $PublicKey;
private $PrivateKey;
private $config;
private $logger;
private UcloudClient $client;
public function __construct($config, $ext = null)
{
$this->PublicKey = $config['PublicKey'];
$this->PrivateKey = $config['PrivateKey'];
$this->client = new UcloudClient($this->PublicKey, $this->PrivateKey);
$this->config = $config;
}
public function register()
{
if (empty($this->PublicKey) || empty($this->PrivateKey) || empty($this->config['username']) || empty($this->config['phone']) || empty($this->config['email'])) throw new Exception('必填参数不能为空');
$param = ['Mode' => 'free'];
$this->request('GetCertificateList', $param);
return true;
}
public function buyCert($domainList, &$order)
{
$param = [
'CertificateBrand' => 'TrustAsia',
'CertificateName' => 'TrustAsiaC1DVFree',
'DomainsCount' => 1,
'ValidYear' => 1,
];
$data = $this->request('PurchaseCertificate', $param);
if (!isset($data['CertificateID'])) throw new Exception('证书购买失败CertificateID为空');
$order['CertificateID'] = $data['CertificateID'];
}
public function createOrder($domainList, &$order, $keytype, $keysize)
{
if (empty($domainList)) throw new Exception('域名列表不能为空');
$domain = $domainList[0];
$param = [
'CertificateID' => $order['CertificateID'],
'Domains' => $domain,
'CSROnline' => 1,
'CSREncryptAlgo' => ['RSA' => 'RSA', 'ECC' => 'ECDSA'][$keytype],
'CSRKeyParameter' => ['2048' => '2048', '3072' => '3072', '256' => 'prime256v1', '384' => 'prime384v1'][$keysize],
'CompanyName' => '公司名称',
'CompanyAddress' => '公司地址',
'CompanyRegion' => '北京',
'CompanyCity' => '北京',
'CompanyCountry' => 'CN',
'CompanyDivision' => '部门',
'CompanyPhone' => $this->config['phone'],
'CompanyPostalCode' => '110100',
'AdminName' => $this->config['username'],
'AdminPhone' => $this->config['phone'],
'AdminEmail' => $this->config['email'],
'AdminTitle' => '职员',
'DVAuthMethod' => 'DNS'
];
$data = $this->request('ComplementCSRInfo', $param);
sleep(3);
$param = [
'CertificateID' => $order['CertificateID'],
];
$data = $this->request('GetDVAuthInfo', $param);
$dnsList = [];
if (!empty($data['Auths'])) {
foreach ($data['Auths'] as $auth) {
$mainDomain = getMainDomain($auth['Domain']);
$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;
}
}

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

@ -0,0 +1,110 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\acme\ACMECert;
use Exception;
class zerossl implements CertInterface
{
private $directory = 'https://acme.zerossl.com/v2/DV90';
private $ac;
private $config;
private $ext;
public function __construct($config, $ext = null)
{
$this->config = $config;
$this->ac = new ACMECert($this->directory, $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);
}
}

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

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

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

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

View File

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

View File

@ -0,0 +1,261 @@
<?php
namespace app\lib\client;
use Exception;
class AliyunOSS
{
private $AccessKeyId;
private $AccessKeySecret;
private $Endpoint;
public function __construct($AccessKeyId, $AccessKeySecret, $Endpoint)
{
$this->AccessKeyId = $AccessKeyId;
$this->AccessKeySecret = $AccessKeySecret;
$this->Endpoint = $Endpoint;
}
public function addBucketCnameCert($bucket, $domain, $cert_id)
{
$strXml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<BucketCnameConfiguration>
</BucketCnameConfiguration>
EOF;
$xml = new \SimpleXMLElement($strXml);
$node = $xml->addChild('Cname');
$node->addChild('Domain', $domain);
$certNode = $node->addChild('CertificateConfiguration');
$certNode->addChild('CertId', $cert_id);
$certNode->addChild('Force', 'true');
$body = $xml->asXML();
$options = [
'bucket' => $bucket,
'key' => '',
];
$query = [
'cname' => '',
'comp' => 'add'
];
return $this->request('POST', '/', $query, $body, $options);
}
public function deleteBucketCnameCert($bucket, $domain)
{
$strXml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<BucketCnameConfiguration>
</BucketCnameConfiguration>
EOF;
$xml = new \SimpleXMLElement($strXml);
$node = $xml->addChild('Cname');
$node->addChild('Domain', $domain);
$certNode = $node->addChild('CertificateConfiguration');
$certNode->addChild('DeleteCertificate', 'true');
$body = $xml->asXML();
$options = [
'bucket' => $bucket,
'key' => '',
];
$query = [
'cname' => '',
'comp' => 'add'
];
return $this->request('POST', '/', $query, $body, $options);
}
public function getBucketCname($bucket)
{
$options = [
'bucket' => $bucket,
'key' => '',
];
$query = [
'cname' => '',
];
return $this->request('GET', '/', $query, null, $options);
}
private function request($method, $path, $query, $body, $options)
{
$hostname = $options['bucket'] . '.' . $this->Endpoint;
$query_string = $this->toQueryString($query);
$query_string = empty($query_string) ? '' : '?' . $query_string;
$requestUrl = 'https://' . $hostname . $path . $query_string;
$headers = [
'Content-Type' => 'application/xml',
'Date' => gmdate('D, d M Y H:i:s \G\M\T'),
];
$headers['Authorization'] = $this->getAuthorization($method, $path, $query, $headers, $options);
$header = [];
foreach ($headers as $key => $value) {
$header[] = $key . ': ' . $value;
}
return $this->curl($method, $requestUrl, $body, $header);
}
private function curl($method, $url, $body, $header)
{
$ch = curl_init($url);
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"
);
}

View File

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

View File

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

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

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

View File

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

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

@ -0,0 +1,51 @@
<?php
namespace app\lib\client;
use Exception;
class Ucloud
{
const VERSION = "0.1.0";
private $ApiUrl = 'https://api.ucloud.cn/';
private $PublicKey;
private $PrivateKey;
public function __construct($PublicKey, $PrivateKey)
{
$this->PublicKey = $PublicKey;
$this->PrivateKey = $PrivateKey;
}
public function request($action, $params)
{
$param = [
'Action' => $action,
'PublicKey' => $this->PublicKey,
];
$param = array_merge($param, $params);
$param['Signature'] = $this->ucloudSignature($param);
$ua = sprintf("PHP/%s PHP-SDK/%s", phpversion(), self::VERSION);
$response = get_curl($this->ApiUrl, json_encode($param), 0, 0, 0, $ua, 0, ['Content-Type: application/json']);
$result = json_decode($response, true);
if (isset($result['RetCode']) && $result['RetCode'] == 0) {
return $result;
} elseif (isset($result['Message'])) {
throw new Exception($result['Message']);
} else {
throw new Exception('返回数据解析失败');
}
}
private function ucloudSignature($param)
{
ksort($param);
$str = '';
foreach ($param as $key => $value) {
$str .= $key . $value;
}
$str .= $this->PrivateKey;
return sha1($str);
}
}

View File

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

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

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

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

@ -0,0 +1,127 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class allwaf implements DeployInterface
{
private $logger;
private $url = 'https://api.allwaf.cn';
private $accessKeyId;
private $accessKey;
private $usertype = 'user';
private $proxy;
private $accessToken;
public function __construct($config)
{
$this->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);
}
}
}

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

@ -0,0 +1,90 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\AWS as AWSClient;
use Exception;
class aws implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $SecretAccessKey;
public function __construct($config)
{
$this->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('<?xml version="1.0" encoding="utf-8"?><DistributionConfig></DistributionConfig>');
$client->requestXmlN('PUT', '/distribution/'.$config['distribution_id'].'/config', $data, $xml);
$this->log('分配ID: ' . $config['distribution_id'] . ' 证书部署成功!');
}
private function get_cert_id($fullchain, $privatekey)
{
$cert = 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);
}
}
}

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

@ -0,0 +1,71 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\BaiduCloud;
use Exception;
class baidu implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $SecretAccessKey;
public function __construct($config)
{
$this->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);
}
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class baishan implements DeployInterface
{
private $logger;
private $url = 'https://cdn.api.baishan.com';
private $token;
private $proxy;
public function __construct($config)
{
$this->token = $config['token'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->token)) throw new Exception('token不能为空');
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
if (empty($config['id'])) throw new Exception('证书ID不能为空');
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
$cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
$params = [
'cert_id' => $config['id'],
'name' => $cert_name,
'certificate' => $fullchain,
'key' => $privatekey,
];
try {
$this->request('/v2/domain/certificate?token=' . $this->token, $params);
} catch (Exception $e) {
if (strpos($e->getMessage(), 'this certificate is exists') !== false) {
$this->log('证书ID:' . $config['id'] . '已存在,无需更新');
return;
}
throw new Exception($e->getMessage());
}
$this->log('证书ID:' . $config['id'] . '更新成功!');
}
private function request($path, $params = null)
{
$url = $this->url . $path;
$headers = [];
$body = null;
if ($params) {
$headers[] = 'Content-Type: application/json';
$body = json_encode($params);
}
$response = 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);
}
}
}

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

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

View File

@ -0,0 +1,67 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class cachefly implements DeployInterface
{
private $logger;
private $url = 'https://api.cachefly.com/api/2.5';
private $apikey;
private $proxy;
public function __construct($config)
{
$this->apikey = $config['apikey'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->apikey)) throw new Exception('API令牌不能为空');
$this->request('/accounts/me');
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$params = [
'certificate' => $fullchain,
'certificateKey' => $privatekey,
];
$this->request('/certificates', $params);
$this->log('证书上传成功!');
}
private function request($path, $params = null, $method = null)
{
$url = $this->url . $path;
$headers = ['x-cf-authorization: Bearer ' . $this->apikey];
$body = null;
if ($params) {
$headers[] = 'Content-Type: application/json';
$body = json_encode($params);
}
$response = 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);
}
}
}

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

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

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

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

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

@ -0,0 +1,113 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class ftp implements DeployInterface
{
private $logger;
private $config;
public function __construct($config)
{
$this->config = $config;
}
public function check()
{
$this->connect();
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$conn_id = $this->connect();
ftp_pasv($conn_id, true);
if ($config['format'] == 'pem') {
$temp_stream = fopen('php://temp', 'r+');
fwrite($temp_stream, $fullchain);
rewind($temp_stream);
if (ftp_fput($conn_id, $config['pem_cert_file'], $temp_stream, FTP_BINARY)) {
$this->log('证书文件上传成功:' . $config['pem_cert_file']);
} else {
fclose($temp_stream);
ftp_close($conn_id);
throw new Exception('证书文件上传失败:' . $config['pem_cert_file']);
}
fclose($temp_stream);
$temp_stream = fopen('php://temp', 'r+');
fwrite($temp_stream, $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;
}
}

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

@ -0,0 +1,77 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class gcore implements DeployInterface
{
private $logger;
private $url = 'https://api.gcore.com';
private $apikey;
private $proxy;
public function __construct($config)
{
$this->apikey = $config['apikey'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->apikey)) throw new Exception('API令牌不能为空');
$this->request('/iam/clients/me');
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$id = $config['id'];
if (empty($id)) throw new Exception('证书ID不能为空');
$params = [
'name' => $config['name'],
'sslCertificate' => $fullchain,
'sslPrivateKey' => $privatekey,
'validate_root_ca' => true,
];
$this->request('/cdn/sslData/' . $id, $params, 'PUT');
$this->log('证书ID:' . $id . '更新成功!');
}
private function request($path, $params = null, $method = null)
{
$url = $this->url . $path;
$headers = ['Authorization: APIKey ' . $this->apikey];
$body = null;
if ($params) {
$headers[] = 'Content-Type: application/json';
$body = json_encode($params);
}
$response = 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);
}
}
}

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

@ -0,0 +1,135 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class goedge implements DeployInterface
{
private $logger;
private $url;
private $accessKeyId;
private $accessKey;
private $usertype;
private $systype;
private $proxy;
private $accessToken;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->accessKeyId = $config['accessKeyId'];
$this->accessKey = $config['accessKey'];
$this->usertype = $config['usertype'];
$this->systype = $config['systype'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->accessKeyId) || empty($this->accessKey)) throw new Exception('必填参数不能为空');
$this->getAccessToken();
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$domains = $config['domainList'];
if (empty($domains)) throw new Exception('没有设置要部署的域名');
$this->getAccessToken();
$params = [
'domains' => $domains,
'offset' => 0,
'size' => 10,
];
try {
$data = $this->request('/SSLCertService/listSSLCerts', $params);
} catch (Exception $e) {
throw new Exception('获取证书列表失败:' . $e->getMessage());
}
$list = json_decode(base64_decode($data['sslCertsJSON']), true);
if (!$list || 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);
}
}
}

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

@ -0,0 +1,147 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\HuaweiCloud;
use Exception;
class huawei implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $SecretAccessKey;
public function __construct($config)
{
$this->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);
}
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\Volcengine;
use Exception;
class huoshan implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $SecretAccessKey;
public function __construct($config)
{
$this->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);
}
}
}

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

@ -0,0 +1,208 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class kangle implements DeployInterface
{
private $logger;
private $url;
private $auth;
private $username;
private $password;
private $skey;
private $proxy;
private $cookie;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->auth = $config['auth'];
$this->username = $config['username'];
$this->password = $config['password'];
$this->skey = $config['skey'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->username)) throw new Exception('必填参数不能为空');
$this->login();
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$this->login();
$this->log('登录成功 cookie:' . $this->cookie);
$this->getMain();
if ($config['type'] == '1' && !empty($config['domains'])) {
$domains = explode("\n", $config['domains']);
$success = 0;
$errmsg = null;
foreach ($domains as $domain) {
$domain = trim($domain);
if (empty($domain)) continue;
try {
$this->deployDomain($domain, $fullchain, $privatekey);
$this->log("域名 {$domain} 证书部署成功");
$success++;
} catch (Exception $e) {
$errmsg = $e->getMessage();
$this->log("域名 {$domain} 证书部署失败:" . $errmsg);
}
}
if ($success == 0) {
throw new Exception($errmsg ? $errmsg : '要部署的域名不存在');
}
} else {
$this->deployAccount($fullchain, $privatekey);
$this->log("账号级SSL证书部署成功");
}
}
private function deployDomain($domain, $fullchain, $privatekey)
{
$path = '/vhost/?c=ssl&a=domainSsl';
$post = [
'domain' => $domain,
'certificate' => $fullchain,
'certificate_key' => $privatekey,
];
$response = 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);
}
}
}

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

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

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

@ -0,0 +1,77 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class local implements DeployInterface
{
private $logger;
public function __construct($config) {}
public function check() {}
public function deploy($fullchain, $privatekey, $config, &$info)
{
if (!empty($config['cmd']) && !function_exists('exec')) {
throw new Exception('exec函数被禁用');
}
if ($config['format'] == 'pem') {
$dir = dirname($config['pem_cert_file']);
if (!is_dir($dir)) throw new Exception($dir.' 目录不存在');
if (!is_writable($dir)) throw new Exception($dir.' 目录不可写');
if (file_put_contents($config['pem_cert_file'], $fullchain)) {
$this->log('证书已保存到:' . $config['pem_cert_file']);
} else {
throw new Exception('证书保存到' . $config['pem_cert_file'] . '失败,请检查目录权限');
}
if (file_put_contents($config['pem_key_file'], $privatekey)) {
$this->log('私钥已保存到:' . $config['pem_key_file']);
} else {
throw new Exception('私钥保存到' . $config['pem_key_file'] . '失败,请检查目录权限');
}
} elseif ($config['format'] == 'pfx') {
$dir = dirname($config['pfx_file']);
if (!is_dir($dir)) throw new Exception($dir.' 目录不存在');
if (!is_writable($dir)) throw new Exception($dir.' 目录不可写');
$pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null);
if (file_put_contents($config['pfx_file'], $pfx)) {
$this->log('PFX证书已保存到' . $config['pfx_file']);
} else {
throw new Exception('PFX证书保存到' . $config['pfx_file'] . '失败,请检查目录权限');
}
}
if (!empty($config['cmd'])) {
$cmds = explode("\n", $config['cmd']);
foreach($cmds as $cmd){
$cmd = trim($cmd);
if(empty($cmd)) continue;
$this->log('执行命令:'.$cmd);
$output = [];
$ret = 0;
exec($cmd, $output, $ret);
if ($ret == 0) {
$this->log('执行命令成功:' . implode("\n", $output));
} else {
throw new Exception('执行命令失败:' . implode("\n", $output));
}
}
}
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
public function setLogger($logger)
{
$this->logger = $logger;
}
}

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

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

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

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

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

@ -0,0 +1,104 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class safeline implements DeployInterface
{
private $logger;
private $url;
private $token;
private $proxy;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->token = $config['token'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->token)) throw new Exception('请填写控制台地址和API Token');
$this->request('/api/open/system');
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$domains = $config['domainList'];
if (empty($domains)) throw new Exception('没有设置要部署的域名');
try {
$data = $this->request('/api/open/cert');
$this->log('获取证书列表成功(total=' . $data['total'] . ')');
} catch (Exception $e) {
throw new Exception('获取证书列表失败:' . $e->getMessage());
}
$success = 0;
$errmsg = null;
foreach ($data['nodes'] as $row) {
if (empty($row['domains'])) continue;
$flag = false;
foreach ($row['domains'] as $domain) {
if (in_array($domain, $domains)) {
$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);
}
}
}

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

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

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

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

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

@ -0,0 +1,132 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use app\lib\client\Ucloud as UcloudClient;
use Exception;
class ucloud implements DeployInterface
{
private $logger;
private $PublicKey;
private $PrivateKey;
private UcloudClient $client;
public function __construct($config)
{
$this->PublicKey = $config['PublicKey'];
$this->PrivateKey = $config['PrivateKey'];
$this->client = new UcloudClient($this->PublicKey, $this->PrivateKey);
}
public function check()
{
if (empty($this->PublicKey) || empty($this->PrivateKey)) throw new Exception('必填参数不能为空');
$param = ['Mode' => 'free'];
$this->client->request('GetCertificateList', $param);
return true;
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$domain_id = $config['domain_id'];
if (empty($domain_id)) throw new Exception('云分发资源ID不能为空');
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
$cert_name = str_replace(['*', '.'], '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
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);
}
}
}

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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'];

View File

@ -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);
}

View File

@ -0,0 +1,140 @@
<?php
namespace app\service;
use Exception;
use think\facade\Db;
use app\lib\DeployHelper;
/**
* SSL证书自动部署
*/
class CertDeployService
{
private static $retry_interval = [60, 300, 600, 1800, 3600];
private $client;
private $aid;
private $task;
private $info;
//任务状态0:待处理 1:已完成 -1:处理失败
public function __construct($tid)
{
$task = Db::name('cert_deploy')->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;
}
}
}

View File

@ -0,0 +1,426 @@
<?php
namespace app\service;
use Exception;
use think\facade\Db;
use app\lib\CertHelper;
use app\utils\CertDnsUtils;
/**
* SSL证书订单处理
*/
class CertOrderService
{
private static $retry_interval = [60, 180, 300, 600, 600];
private $client;
private $aid;
private $atype;
private $order;
private $info;
private $dnsList;
private $domainList;
private $cnameDomainList = [];
// 订单状态0:待提交 1:待验证 2:正在验证 3:已签发 4:已吊销 -1:购买证书失败 -2:创建订单失败 -3:添加DNS失败 -4:验证DNS失败 -5:验证订单失败 -6:订单验证未通过 -7:签发证书失败
public function __construct($oid)
{
$order = Db::name('cert_order')->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;
}
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace app\service;
use Exception;
use think\facade\Db;
use app\utils\MsgNotice;
class CertTaskService
{
public function execute()
{
$this->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<NOW()) OR status=3 AND expiretime<:expiretime', ['expiretime' => 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 retrytime<NOW())')->select();
//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);
}
}
}

View File

@ -1,10 +1,14 @@
<?php
namespace app\lib;
namespace app\service;
use Exception;
use think\facade\Db;
use app\lib\DnsHelper;
/**
* CF优选IP
*/
class OptimizeService
{
private $ip_address = [];

View File

@ -1,12 +1,15 @@
<?php
namespace app\lib;
namespace app\service;
use app\lib\NewDb;
use app\lib\CheckUtils;
use app\lib\DnsHelper;
use app\lib\MsgNotice;
use app\utils\CheckUtils;
use app\utils\MsgNotice;
/**
* 容灾监控任务执行
*/
class TaskRunner
{
private $conn;

View File

@ -5,7 +5,7 @@ CREATE TABLE `dnsmgr_config` (
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `dnsmgr_config` VALUES ('version', '1011');
INSERT INTO `dnsmgr_config` VALUES ('version', '1021');
INSERT INTO `dnsmgr_config` VALUES ('notice_mail', '0');
INSERT INTO `dnsmgr_config` VALUES ('notice_wxtpl', '0');
INSERT INTO `dnsmgr_config` VALUES ('mail_smtp', 'smtp.qq.com');
@ -48,6 +48,8 @@ CREATE TABLE `dnsmgr_user` (
`level` int(11) NOT NULL DEFAULT '0',
`regtime` datetime DEFAULT NULL,
`lasttime` datetime DEFAULT NULL,
`totp_open` tinyint(1) NOT NULL DEFAULT '0',
`totp_secret` varchar(100) DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
KEY `username` (`username`)
@ -135,4 +137,89 @@ CREATE TABLE `dnsmgr_optimizeip` (
`errmsg` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `did` (`did`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `dnsmgr_cert_account`;
CREATE TABLE `dnsmgr_cert_account` (
`id` int(11) unsigned NOT NULL auto_increment,
`type` varchar(20) NOT NULL,
`name` varchar(255) NOT NULL,
`config` text DEFAULT NULL,
`ext` text DEFAULT NULL,
`remark` varchar(100) DEFAULT NULL,
`deploy` tinyint(1) NOT NULL DEFAULT '0',
`addtime` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `dnsmgr_cert_order`;
CREATE TABLE `dnsmgr_cert_order` (
`id` int(11) unsigned NOT NULL auto_increment,
`aid` int(11) unsigned NOT NULL,
`keytype` varchar(20) DEFAULT NULL,
`keysize` varchar(20) DEFAULT NULL,
`addtime` datetime DEFAULT NULL,
`updatetime` datetime DEFAULT NULL,
`processid` varchar(32) DEFAULT NULL,
`issuetime` datetime DEFAULT NULL,
`expiretime` datetime DEFAULT NULL,
`issuer` varchar(100) NOT NULL,
`status` tinyint(1) NOT NULL DEFAULT '0',
`error` varchar(300) DEFAULT NULL,
`isauto` tinyint(1) NOT NULL DEFAULT '0',
`retry` tinyint(4) NOT NULL DEFAULT '0',
`retry2` tinyint(4) NOT NULL DEFAULT '0',
`retrytime` datetime DEFAULT NULL,
`islock` tinyint(1) NOT NULL DEFAULT '0',
`locktime` datetime DEFAULT NULL,
`issend` tinyint(1) NOT NULL DEFAULT '0',
`info` text DEFAULT NULL,
`dns` text DEFAULT NULL,
`fullchain` text DEFAULT NULL,
`privatekey` text DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `dnsmgr_cert_domain`;
CREATE TABLE `dnsmgr_cert_domain` (
`id` int(11) unsigned NOT NULL auto_increment,
`oid` int(11) unsigned NOT NULL,
`domain` varchar(255) NOT NULL,
`sort` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `oid` (`oid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `dnsmgr_cert_deploy`;
CREATE TABLE `dnsmgr_cert_deploy` (
`id` int(11) unsigned NOT NULL auto_increment,
`aid` int(11) unsigned NOT NULL,
`oid` int(11) unsigned NOT NULL,
`issuetime` datetime DEFAULT NULL,
`config` text DEFAULT NULL,
`remark` varchar(100) DEFAULT NULL,
`addtime` datetime DEFAULT NULL,
`lasttime` datetime DEFAULT NULL,
`processid` varchar(32) DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
`error` varchar(300) DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT 0,
`retry` tinyint(4) NOT NULL DEFAULT '0',
`retrytime` datetime DEFAULT NULL,
`islock` tinyint(1) NOT NULL DEFAULT '0',
`locktime` datetime DEFAULT NULL,
`issend` tinyint(1) NOT NULL DEFAULT '0',
`info` text DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `dnsmgr_cert_cname`;
CREATE TABLE `dnsmgr_cert_cname` (
`id` int(11) unsigned NOT NULL auto_increment,
`domain` varchar(255) NOT NULL,
`did` int(11) unsigned NOT NULL,
`rr` varchar(128) NOT NULL,
`addtime` datetime DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -65,4 +65,88 @@ ALTER TABLE `dnsmgr_domain`
ADD COLUMN `remark` varchar(100) DEFAULT NULL;
ALTER TABLE `dnsmgr_dmtask`
ADD COLUMN `proxy` tinyint(1) NOT NULL DEFAULT 0;
ADD COLUMN `proxy` tinyint(1) NOT NULL DEFAULT 0;
ALTER TABLE `dnsmgr_user`
ADD COLUMN `totp_open` tinyint(1) NOT NULL DEFAULT '0',
ADD COLUMN `totp_secret` varchar(100) DEFAULT NULL;
CREATE TABLE IF NOT EXISTS `dnsmgr_cert_account` (
`id` int(11) unsigned NOT NULL auto_increment,
`type` varchar(20) NOT NULL,
`name` varchar(255) NOT NULL,
`config` text DEFAULT NULL,
`ext` text DEFAULT NULL,
`remark` varchar(100) DEFAULT NULL,
`deploy` tinyint(1) NOT NULL DEFAULT '0',
`addtime` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `dnsmgr_cert_order` (
`id` int(11) unsigned NOT NULL auto_increment,
`aid` int(11) unsigned NOT NULL,
`keytype` varchar(20) DEFAULT NULL,
`keysize` varchar(20) DEFAULT NULL,
`addtime` datetime DEFAULT NULL,
`updatetime` datetime DEFAULT NULL,
`processid` varchar(32) DEFAULT NULL,
`issuetime` datetime DEFAULT NULL,
`expiretime` datetime DEFAULT NULL,
`issuer` varchar(100) NOT NULL,
`status` tinyint(1) NOT NULL DEFAULT '0',
`error` varchar(300) DEFAULT NULL,
`isauto` tinyint(1) NOT NULL DEFAULT '0',
`retry` tinyint(4) NOT NULL DEFAULT '0',
`retry2` tinyint(4) NOT NULL DEFAULT '0',
`retrytime` datetime DEFAULT NULL,
`islock` tinyint(1) NOT NULL DEFAULT '0',
`locktime` datetime DEFAULT NULL,
`issend` tinyint(1) NOT NULL DEFAULT '0',
`info` text DEFAULT NULL,
`dns` text DEFAULT NULL,
`fullchain` text DEFAULT NULL,
`privatekey` text DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `dnsmgr_cert_domain` (
`id` int(11) unsigned NOT NULL auto_increment,
`oid` int(11) unsigned NOT NULL,
`domain` varchar(255) NOT NULL,
`sort` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `oid` (`oid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `dnsmgr_cert_deploy` (
`id` int(11) unsigned NOT NULL auto_increment,
`aid` int(11) unsigned NOT NULL,
`oid` int(11) unsigned NOT NULL,
`issuetime` datetime DEFAULT NULL,
`config` text DEFAULT NULL,
`remark` varchar(100) DEFAULT NULL,
`addtime` datetime DEFAULT NULL,
`lasttime` datetime DEFAULT NULL,
`processid` varchar(32) DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
`error` varchar(300) DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT 0,
`retry` tinyint(4) NOT NULL DEFAULT '0',
`retrytime` datetime DEFAULT NULL,
`islock` tinyint(1) NOT NULL DEFAULT '0',
`locktime` datetime DEFAULT NULL,
`issend` tinyint(1) NOT NULL DEFAULT '0',
`info` text DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `dnsmgr_cert_cname` (
`id` int(11) unsigned NOT NULL auto_increment,
`domain` varchar(255) NOT NULL,
`did` int(11) unsigned NOT NULL,
`rr` varchar(128) NOT NULL,
`addtime` datetime DEFAULT NULL,
`status` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

151
app/utils/CertDnsUtils.php Normal file
View File

@ -0,0 +1,151 @@
<?php
namespace app\utils;
use Exception;
use think\facade\Db;
use app\lib\DnsHelper;
class CertDnsUtils
{
public static function addDns($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) {
$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;
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace app\lib;
namespace app\utils;
class CheckUtils
{

View File

@ -0,0 +1,59 @@
<?php
namespace app\utils;
class DnsQueryUtils
{
private static $doh_servers = ['https://dns.alidns.com/resolve', 'https://doh.pub/resolve', 'https://doh.360.cn/resolve'];
public static function get_dns_records($domain, $type)
{
$dns_type = ['A' => 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;
}
}

View File

@ -1,6 +1,9 @@
<?php
namespace app\lib;
namespace app\utils;
use think\facade\Db;
use app\lib\CertHelper;
use app\lib\DeployHelper;
class MsgNotice
{
@ -55,6 +58,80 @@ class MsgNotice
}
}
public static function cert_send($id, $result)
{
$row = Db::name('cert_order')->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证书已签发成功<br/>证书账户:'.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')<br/>证书域名:'.implode('、', $domainList).'<br/>签发时间:'.$row['issuetime'].'<br/>到期时间:'.$row['expiretime'].'<br/>颁发机构:'.$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']].'<br/>证书账户:'.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')<br/>证书域名:'.implode('、', $domainList).'<br/>失败时间:'.date('Y-m-d H:i:s').'<br/>失败原因:'.$row['error'];
}
$mail_content .= '<br/>'.self::$sitename.'<br/>'.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(['<br/>', '<b>', '</b>'], ["\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('<br/>', "\n", $mail_content);
$content = "<strong>" . $mail_title . "</strong>\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.'<br/>自动部署账户:['.$account['id'].']'.$typename.'('.($account['remark']?$account['remark']:$account['name']).')<br/>关联SSL证书['.$row['oid'].']'.implode('、', $domainList).'<br/>任务备注:'.($row['remark']?$row['remark']:'无');
} else {
$mail_content = '尊敬的用户您好您的SSL证书部署失败<br/>失败原因:'.$row['error'].'<br/>自动部署账户:['.$account['id'].']'.$typename.'('.($account['remark']?$account['remark']:$account['name']).')<br/>关联SSL证书['.$row['oid'].']'.implode('、', $domainList).'<br/>任务备注:'.($row['remark']?$row['remark']:'无');
}
$mail_content .= '<br/>'.self::$sitename.'<br/>'.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(['<br/>', '<b>', '</b>'], ["\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('<br/>', "\n", $mail_content);
$content = "<strong>" . $mail_title . "</strong>\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');

View File

@ -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}
</style>
</head>
<body>
@ -56,18 +59,49 @@ a{color:#444}
<div class="input-group-addon"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></div>
<input type="text" class="form-control input-lg" placeholder="验证码" name="code" autocomplete="off" required="required"/>
<span class="input-group-addon" style="padding: 0">
<img id="verifycode" src="{:captcha_src()}" height="45" onclick="this.src='{:captcha_src()}?r='+Math.random();" title="点击更换验证码">
<img id="verifycode" src="/verifycode" height="45" onclick="this.src='/verifycode?r='+Math.random();" title="点击更换验证码">
</span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success btn-lg btn-block" id="submit" style="background:#708eea;">登 录</button>
</div>
<div class="pull-right"><a href="javascript:findpwd()">忘记密码</a></div>
</form>
<form action="" id="totp-form" onsubmit="return doTotp()" style="display:none;">
<div class="alert alert-info" role="alert">TOTP二次验证</div>
<div class="input-group">
<div class="input-group-addon"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></div>
<input type="number" class="form-control input-lg" placeholder="输入动态口令" name="totp_code" id="totp_code" autocomplete="off" required="required"/>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success btn-lg btn-block" id="submit" style="background:#708eea;">登 录</button>
</div>
<div class="pull-right"><a href="javascript:findpwd()">忘记密码</a></div>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="modal" id="modal-findpwd">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">忘记密码</h4>
</div>
<div class="modal-body">
<strong>重置密码</strong>
<p>进入网站根目录,执行命令:<br/><code>php think reset pwd 用户名 密码</code></p>
<strong>关闭TOTP</strong>
<p>进入网站根目录,执行命令:<br/><code>php think reset totp 用户名</code></p>
</div>
</div>
</div>
</div>
<script src="{$cdnpublic}twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script>
function doLogin(){
@ -100,6 +134,11 @@ function doLogin(){
}else{
if(data.vcode==1){
$("#verifycode").attr('src', '/verifycode?r='+Math.random())
}else if(data.vcode==2){
$("#totp-form").show();
$("#login-form").hide();
$("#totp_code").focus();
return false;
}
layer.alert(data.msg, {icon: 2});
}
@ -107,6 +146,35 @@ function doLogin(){
});
return false;
}
function doTotp(){
var code = $("#totp_code").val();
if(code.length != 6){
layer.msg('动态口令格式错误', {icon: 2});
return false;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.post('/auth/totp', {code:code}, function(res){
layer.close(ii);
if(res.code == 0){
layer.msg('登录成功,正在跳转到首页', {icon: 1,shade: 0.01,time: 15000});
window.location.href = '/';
}else{
layer.alert(res.msg, {icon: 2});
}
});
return false;
}
function findpwd(){
$('#modal-findpwd').modal('show');
}
$(document).ready(function(){
$("#totp_code").keyup(function(){
var code = $(this).val();
if(code.length == 6){
$("#totp-form").submit();
}
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,237 @@
{extend name="common/layout" /}
{block name="title"}{$title}{/block}
{block name="main"}
<style>
.tips{color: #f6a838; padding-left: 5px;}
.input-note{color: green;}
.control-label[is-required]:before {
content: "*";
color: #f56c6c;
margin-right: 4px;
}
</style>
<div class="row" id="app">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title"><a href="javascript:window.history.back()" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>{if $action=='edit'}编辑{else}添加{/if}{$title}</h3></div>
<div class="panel-body">
<form onsubmit="return false" method="post" class="form-horizontal" role="form" id="accountform">
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" is-required>账户类型</label>
<div class="col-sm-6">
<select name="type" v-model="set.type" class="form-control" required :disabled="action=='edit'">
<optgroup v-for="item in typeOption" :label="item.label"><option v-for="item2 in item.children" :value="item2.value">{{item2.label}}</option></optgroup>
</select>
</div>
</div>
<div v-for="(item,name) in inputs" v-show="isShow(item.show)">
<div class="form-group" v-if="item.type=='input'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<input type="text" class="form-control" :name="name" v-model="config[name]" :placeholder="item.placeholder" :required="item.required" :disabled="item.disabled" :data-bv-id="item.validator=='id'" :data-bv-phone="item.validator=='phone'" :data-bv-numeric="item.validator=='numeric'" :data-bv-digits="item.validator=='digits'" :data-bv-integer="item.validator=='integer'" :data-bv-email="item.validator=='email'" :data-bv-uri="item.validator=='uri'" :min="item.min" :max="item.max"><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='textarea'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<textarea class="form-control" :name="name" v-model="config[name]" :placeholder="item.placeholder" :required="item.required" :disabled="item.disabled"></textarea><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='select'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<select class="form-control" :name="name" v-model="config[name]" :required="item.required" :disabled="item.disabled" :placeholder="item.placeholder">
<option v-for="option in item.options" :value="option.value">{{option.label}}</option>
</select><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='radio'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<label class="radio-inline" v-for="(optionname, optionvalue) in item.options">
<input type="radio" :name="name" :value="optionvalue" v-model="config[name]" :disabled="item.disabled"> {{optionname}}
</label><br/><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='checkbox'">
<div class="col-sm-offset-3 col-sm-7">
<div class="checkbox">
<label>
<input type="checkbox" :name="name" v-model="config[name]" :disabled="item.disabled"> {{item.name}}
</label>
</div>
</div>
</div>
<div class="form-group" v-if="item.type=='checkboxes'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<label class="checkbox-inline" v-for="(optionname, optionvalue) in item.options">
<input type="checkbox" :name="name" :value="optionvalue" v-model="config[name]" :disabled="item.disabled"> {{optionname}}
</label><br/><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">备注</label>
<div class="col-sm-6">
<input type="text" name="remark" v-model="set.remark" placeholder="可留空" class="form-control">
</div>
</div>
<div class="form-group" v-show="note">
<div class="col-sm-offset-3 col-sm-6">
<div class="alert alert-dismissible alert-info">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<strong>提示:</strong><span v-html="note"></span>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-6"><button type="button" class="btn btn-primary" @click="submit">提交</button></div>
</div>
</form>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>
var info = {$info|json_encode|raw};
var typeList = {$typeList|json_encode|raw};
var classList = {$classList|json_encode|raw};
new Vue({
el: '#app',
data: {
action: '{$action}',
set: {
deploy: '{$deploy}',
id: '',
type: '',
name: '',
config : '',
remark: '',
},
inputs: {},
config: {},
typeList: typeList,
classList: classList,
note: '',
typeOption: [],
},
watch: {
'set.type': function(val){
if(this.action == 'add' && val && typeList[val]){
this.inputs = typeList[val].inputs;
this.note = typeList[val].note;
this.config = {};
$.each(this.inputs, (name, item) => {
if(typeof item.value == 'undefined'){
if(item.type == 'checkbox'){
item.value = false;
}else if(item.type == 'checkboxes'){
item.value = [];
}else{
item.value = null;
}
}
this.$set(this.config, name, item.value)
})
}
}
},
mounted() {
this.typeOption = Object.keys(classList).map((key) => {
var tempList = [];
Object.keys(typeList).forEach((key2) => {
if(typeList[key2].class == key){
tempList.push({label: typeList[key2].name, value: key2})
}
})
return {label: classList[key], children: tempList}
})
console.log(this.typeOption);
if(this.action == 'edit'){
Object.keys(info).forEach((key) => {
this.set[key] = info[key]
})
var config = JSON.parse(info.config);
this.inputs = typeList[this.set.type].inputs;
this.note = typeList[this.set.type].note;
$.each(this.inputs, (name, item) => {
item.value = config[name];
if(typeof item.value == 'undefined'){
if(item.type == 'checkbox'){
item.value = false;
}else if(item.type == 'checkboxes'){
item.value = [];
}else{
item.value = null;
}
}
this.$set(this.config, name, item.value)
})
}else{
this.set.type = Object.keys(typeList)[0]
}
this.$nextTick(function () {
$('[data-toggle="tooltip"]').tooltip();
})
},
methods: {
submit(){
var that=this;
Object.keys(this.config).forEach((key) => {
if(this.config[key] && typeof this.config[key] == 'string'){
this.config[key] = this.trim(this.config[key]);
}
})
this.set.config = JSON.stringify(this.config);
this.set.name = this.config[Object.keys(this.config)[0]];
let loading = layer.msg('正在进行账户有效性检查', {icon: 16,shade: 0.1,time: 0});
$.ajax({
type: "POST",
url: "",
data: this.set,
dataType: 'json',
success: function(data) {
layer.close(loading);
if(data.code == 0){
layer.alert(data.msg, {icon: 1}, function(){
if(data.msg.indexOf('自动部署账户')>0){
window.location.href = '/cert/deployaccount';
}else{
window.location.href = '/cert/certaccount';
}
});
}else{
layer.alert(data.msg, {icon: 2});
}
},
error: function(data){
layer.close(loading);
layer.msg('服务器错误');
}
});
},
isShow(show){
if(typeof show == 'boolean' && show){
return show;
}else if(typeof show == 'string' && show){
var that=this;
Object.keys(this.config).forEach((key) => {
show = show.replace(new RegExp(key, 'g'), 'that.config["'+key+'"]')
})
return eval(show);
}else{
return true;
}
},
trim(str){
return str.replace(/(^\s*)|(\s*$)/g, "");
}
},
});
</script>
{/block}

View File

@ -0,0 +1,93 @@
{extend name="common/layout" /}
{block name="title"}SSL证书账户管理{/block}
{block name="main"}
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default panel-intro">
<div class="panel-body">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="kw" placeholder="账户名称或备注">
</div>
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
<a href="javascript:searchClear()" class="btn btn-default" title="刷新SSL证书账户列表"><i class="fa fa-refresh"></i> 刷新</a>
<a href="/cert/account/add?deploy=0" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
</form>
<table id="listTable">
</table>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/bootstrap-table.min.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 15;
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
$("#listTable").bootstrapTable({
url: '/cert/account/data?deploy=0',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bordered',
columns: [
{
field: 'id',
title: 'ID'
},
{
field: 'typename',
title: '所属平台',
formatter: function(value, row, index) {
return '<img src="/static/images/'+row.icon+'" class="type-logo"></img>'+value;
}
},
{
field: 'name',
title: '账户名称'
},
{
field: 'remark',
title: '备注'
},
{
field: 'addtime',
title: '添加时间'
},
{
field: '',
title: '操作',
formatter: function(value, row, index) {
var html = '<a href="/cert/account/edit?deploy=0&id='+row.id+'" class="btn btn-info btn-xs">编辑</a> <a href="javascript:delItem('+row.id+')" class="btn btn-danger btn-xs">删除</a>';
return html;
}
},
]
})
})
function delItem(id){
layer.confirm('确定要删除此账户吗?', {
btn: ['确定','取消']
}, function(){
$.post('/cert/account/del?deploy=0', {id: id}, function(data){
if(data.code == 0) {
layer.msg('删除成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
});
}
</script>
{/block}

View File

@ -0,0 +1,385 @@
{extend name="common/layout" /}
{block name="title"}SSL证书订单列表{/block}
{block name="main"}
<style>
tbody tr>td:nth-child(4){overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:180px;}
.tips{cursor:pointer;}
textarea.form-control{margin-bottom: 3px;}
hr{margin-top: 10px;margin-bottom: 15px;border-top: 1px solid #eee;}
.modal-log{padding: 15px 15px 0 15px}
pre.pre-log{height: 330px;overflow-y: auto;width: 100%;background-color: rgba(51, 51, 51, 1);white-space: pre-line;color: rgba(236, 236, 236, 1)}
</style>
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default panel-intro">
<div class="panel-body">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<input type="hidden" name="id" value="">
<div class="form-group">
<label>搜索</label>
<div class="form-group">
<input type="text" class="form-control" name="domain" placeholder="域名">
</div>
</div>
<div class="form-group">
<select name="type" class="form-control"><option value="">所有平台</option>{foreach $types as $k=>$v}
<option value="{$k}">{$v}</option>
{/foreach}</select>
</div>
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
<a href="javascript:searchClear()" class="btn btn-default" title="刷新订单列表"><i class="fa fa-refresh"></i> 刷新</a>
<a href="/cert/order/add" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
</form>
<table id="listTable">
</table>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/bootstrap-table.min.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="{$cdnpublic}FileSaver.js/2.0.5/FileSaver.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 10;
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
$("#listTable").bootstrapTable({
url: '/cert/order/data',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bordered',
uniqueId: 'id',
columns: [
{
field: 'id',
title: 'ID'
},
{
field: 'typename',
title: '证书账户',
formatter: function(value, row, index) {
return '<span title="'+row.aremark+'" data-toggle="tooltip" data-placement="right"><img src="/static/images/'+row.icon+'" class="type-logo">'+value+'('+row.aid+')</span>';
}
},
{
field: 'domains',
title: '绑定域名',
formatter: function(value, row, index) {
return value.join('<br/>');
}
},
{
field: 'keytype',
title: '证书信息',
formatter: function(value, row, index) {
return '<span class="text-muted">签名算法:</span>'+row.keytype+'('+row.keysize+')'+(row.issuer?'<br/><span class="text-muted">颁发机构:</span>'+row.issuer:'');
}
},
{
field: 'isauto',
title: '自动续签',
formatter: function(value, row, index) {
if(value == 1){
return '<div class="material-switch"><input id="isauto'+row.id+'" type="checkbox" checked onchange="setAuto('+row.id+',0)"/><label for="isauto'+row.id+'" class="label-primary"></label></div>';
}else{
return '<div class="material-switch"><input id="isauto'+row.id+'" type="checkbox" onchange="setAuto('+row.id+',1)"/><label for="isauto'+row.id+'" class="label-primary"></label></div>';
}
}
},
{
field: 'issuetime',
title: '签发时间',
formatter: function(value, row, index) {
return value ? value.substring(0,10) : '暂未签发';
}
},
{
field: 'end_day',
title: '到期时间',
formatter: function(value, row, index) {
if(value){
if(value > 7){
return '<span title="'+row.expiretime+'" data-toggle="tooltip" data-placement="right" style="color:green">剩余' + value + '天<span>';
}else if(value > 0){
return '<span title="'+row.expiretime+'" data-toggle="tooltip" data-placement="right" style="color:#ff7f00">剩余' + value + '天<span>';
}else{
return '<span title="'+row.expiretime+'" data-toggle="tooltip" data-placement="right" style="color:red">已过期<span>';
}
}else{
return '暂未签发';
}
}
},
{
field: 'status',
title: '状态',
formatter: function(value, row, index) {
if(value == 4) {
return '<span class="label" style="background-color: #a5a5a5;">已吊销</span>';
} else if(value == 3) {
return '<span class="label label-success">已签发</span>';
} else if(value == 2) {
return '<span class="label" style="background-color: #3e76fb;">正在验证</span>';
} else if(value == 1) {
return '<span class="label" style="background-color: #3e76fb;">待验证</span>';
} else if(value == 0) {
return '<span class="label label-info">待提交</span>';
} else {
var title = '失败';
if(value == -1) title = '购买证书失败';
else if(value == -2) title = '创建订单失败';
else if(value == -3) title = '添加DNS失败';
else if(value == -4) title = '验证DNS失败';
else if(value == -5) title = '验证订单失败';
else if(value == -6) title = '订单验证未通过';
else if(value == -7) title = '签发证书失败';
return '<span class="label label-danger">'+title+'</span>'+(row.error?' <span onclick="showmsg(\''+row.error+'\')" class="tips" title="失败原因"><i class="fa fa-info-circle"></i></span>':'');
}
}
},
{
field: '',
title: '操作',
formatter: function(value, row, index) {
var html = '';
if(row.status == 0) {
html += '<a href="javascript:doOrder(\''+row.id+'\')" class="btn btn-success btn-xs"><i class="fa fa-play-circle"></i> 立即提交</a>&nbsp;&nbsp;';
}else if(row.status == 1) {
html += '<a href="javascript:doOrder(\''+row.id+'\')" class="btn btn-success btn-xs"><i class="fa fa-check-circle"></i> 提交验证</a>&nbsp;&nbsp;';
}else if(row.status == 2) {
html += '<a href="javascript:doOrder(\''+row.id+'\')" class="btn btn-success btn-xs"><i class="fa fa-check-circle"></i> 继续验证</a>&nbsp;&nbsp;';
}else if(row.status == 3) {
html += '<a href="javascript:download(\''+row.id+'\')" class="btn btn-success btn-xs"><i class="fa fa-download"></i> 下载</a>&nbsp;&nbsp;<a href="javascript:renewOrder(\''+row.id+'\')" class="btn btn-warning btn-xs"><i class="fa fa-refresh"></i> 续签</a>&nbsp;&nbsp;';
}else if(row.status == 4) {
html += '<a href="javascript:renewOrder(\''+row.id+'\')" class="btn btn-success btn-xs"><i class="fa fa-play-circle"></i> 重新申请</a>&nbsp;&nbsp;';
}else{
html += '<a href="javascript:doOrder(\''+row.id+'\')" class="btn btn-success btn-xs"><i class="fa fa-repeat"></i> 重试</a>&nbsp;&nbsp;';
}
html += '<a href="/cert/order/edit?id='+row.id+'" class="btn btn-primary btn-xs"><i class="fa fa-edit"></i> 修改</a>&nbsp;&nbsp;';
html += '<div class="btn-group dropdown-group" role="group"><button type="button" class="btn btn-info btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">更多 <span class="caret"></span></button><ul class="dropdown-menu">';
html += '<li><a href="javascript:showLog(\''+row.processid+'\')">查看日志</a></li>';
if(row.status == 3){
html += '<li><a href="/cert/deploytask?oid='+row.id+'">部署任务</a></li>';
html += '<li><a href="javascript:revokeOrder(\''+row.id+'\')">吊销证书</a></li>';
}else if(row.status < 0){
html += '<li><a href="javascript:resetOrder(\''+row.id+'\')">重置订单</a></li>';
}else if(row.status == 1 || row.status == 2){
html += '<li><a href="javascript:resetOrder(\''+row.id+'\')">取消订单</a></li>';
}
html += '<li><a href="javascript:delItem('+row.id+','+row.status+')">删除</a></li>';
html += '</ul></div>';
return html;
}
},
],
onLoadSuccess: function(data) {
$('[data-toggle="tooltip"]').tooltip()
$('.dropdown-group').on('show.bs.dropdown', function (e) {
var btnPos = $(e.target)[0].getBoundingClientRect();
var screenWidth = $(window).width();
var screenHeight = $(window).height();
var childrenWidth = $(e.target).children('.dropdown-menu').width();
var childrenHeight = $(e.target).children('.dropdown-menu').height();
var top = btnPos.bottom;
if(top + childrenHeight + 12 > screenHeight){
top = btnPos.top - childrenHeight - 12;
}
var left = btnPos.left;
if(left + childrenWidth + 7 > screenWidth){
left = screenWidth - childrenWidth - 7;
}
$(e.target).children('.dropdown-menu').css({position:'fixed', top:top, left:left});
});
}
})
})
function showmsg(msg){
layer.alert(msg, {icon: 0, title: '失败原因'});
}
function setAuto(id, status){
$.post('/cert/order/setauto', {id: id, isauto: status}, function(data){
if(data.code == 0) {
layer.msg('已'+(status==1?'开启':'关闭')+'自动续签', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
}
function delItem(id,status){
var msg = '确定要删除此证书订单吗?';
if(status == 3) msg += '删除后将无法再次下载证书';
layer.confirm(msg, {
btn: ['确定','取消']
}, function(){
$.post('/cert/order/del', {id: id}, function(data){
if(data.code == 0) {
layer.msg('删除成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
});
}
function doOrder(id, reset){
reset = reset || 0;
var ii = layer.msg('正在处理证书订单...', {icon: 16,shade: 0.1,time: 0});
$.ajax({
type: "POST",
url: "/cert/order/process",
data: {id: id, reset: reset},
dataType: 'json',
success: function(data) {
layer.close(ii);
if(data.code == 0) {
layer.alert(data.msg, {icon: 1});
$('#listTable').bootstrapTable('refresh');
} else {
if(data.msg == '订单正在处理中,请稍后再试'){
layer.alert(data.msg, {icon: 2}, function(){
layer.closeAll();
var row = $("#listTable").bootstrapTable('getRowByUniqueId', id);
showLog(row.processid)
});
}else{
layer.alert(data.msg, {icon: 2});
$('#listTable').bootstrapTable('refresh');
}
}
},
error: function(data){
layer.close(ii);
layer.msg('执行超时,请稍后刷新列表或查看日志', {icon:0});
}
});
}
function resetOrder(id){
layer.confirm('重置订单后,订单将变成待提交状态,是否确定?', {
btn: ['确定','取消'], title: '重置订单', icon: 0
}, function(){
$.post('/cert/order/reset', {id: id}, function(data){
if(data.code == 0) {
layer.msg('重置订单状态成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
});
}
function revokeOrder(id){
layer.confirm('是否确定要吊销该证书?吊销后浏览器将不再信任该证书', {
btn: ['确定','取消'], title: '吊销证书', icon: 0
}, function(){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.post('/cert/order/revoke', {id: id}, function(data){
layer.close(ii);
if(data.code == 0) {
layer.alert('吊销证书成功!', {icon: 1});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
});
}
function renewOrder(id){
layer.confirm('是否确定重新申请该证书?', {
btn: ['确定','取消'], title: '续签证书', icon: 0
}, function(){
doOrder(id, 1);
});
}
function download(id){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.post('/cert/order/get', {id: id}, function(data){
layer.close(ii);
if(data.code == 0) {
layer.open({
type: 1,
title: '下载证书',
area: [$(window).width() > 768 ? '600px' : '100%', '360px'],
shadeClose: true,
content: '<div class="modal-body"><div class="row text-center"><div class="form-group col-xs-6"><label>证书(PEM格式)</label><textarea rows="6" class="form-control" name="fullchain">'+data.data.fullchain+'</textarea><a onclick="copy(\'fullchain\')" class="btn btn-default"><i class="fa fa-copy"></i> 复制</a>&nbsp;&nbsp;<a onclick="downloadFile(\'fullchain.crt\',\'fullchain\')" class="btn btn-default"><i class="fa fa-download"></i> 下载</a></div><div class="form-group col-xs-6"><label>私钥(PEM格式)</label><textarea rows="6" class="form-control" name="privatekey">'+data.data.privatekey+'</textarea><a onclick="copy(\'privatekey\')" class="btn btn-default"><i class="fa fa-copy"></i> 复制</a>&nbsp;&nbsp;<a onclick="downloadFile(\'private.key\',\'privatekey\')" class="btn btn-default"><i class="fa fa-download"></i> 下载</a></div></div><hr/><label>IIS服务器(pfx格式)</label><input type="hidden" name="pfx" value="'+data.data.pfx+'"><a onclick="downloadPFX()" class="btn btn-default"><i class="fa fa-download"></i> 下载</a>密码为123456</div>',
});
} else {
layer.alert(data.msg, {icon: 2});
}
}, 'json');
}
function copy(obj){
$("textarea[name='"+obj+"']").select();
document.execCommand("Copy");
layer.msg('复制成功', {icon:1, time:600});
}
function downloadFile(filename,obj){
var content = $("textarea[name='"+obj+"']").val();
if(!content) return;
var blob = new Blob([content], {type: "application/force-download"});
saveAs(blob, filename);
}
function downloadPFX(id){
var content = $("input[name='pfx']").val();
if(!content) return;
var bstr = atob(content),
n = bstr.length,
u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
var filename = 'cert.pfx';
var blob = new Blob([u8arr], {type: "application/force-download"});
saveAs(blob, filename);
}
var intverval;
function showLog(processid){
if(processid == '' || processid == 'null'){
layer.msg('暂无日志', {time: 600});
return;
}
$.post('/cert/order/show_log', {processid: processid}, function(data){
if(data.code == 0) {
layer.closeAll();
var filemtime = data.time;
layer.open({
type: 1,
title: '查看日志',
area: [$(window).width() > 768 ? '600px' : '100%', '400px'],
shadeClose: true,
resize: false,
content: '<div class="modal-log"><pre class="pre-log" id="execLog">'+data.data+'</pre></div>',
success: function(){
var exec_log = $('#execLog');
exec_log[0].scrollTop = exec_log[0].scrollHeight
intverval = setInterval(function(){
$.post('/cert/order/show_log', {processid: processid}, function(data){
if(data.code == 0 && data.time != filemtime) {
var exec_log = $('#execLog');
exec_log.html(data.data);
filemtime = data.time;
exec_log[0].scrollTop = exec_log[0].scrollHeight
}
}, 'json');
}, 1500)
},
end: function(){
clearInterval(intverval);
}
});
} else {
layer.msg(data.msg, {icon: 2, time: 600});
}
}, 'json');
}
</script>
{/block}

115
app/view/cert/certset.html Normal file
View File

@ -0,0 +1,115 @@
{extend name="common/layout" /}
{block name="title"}SSL证书计划任务{/block}
{block name="main"}
<div class="row">
<div class="col-xs-12 col-sm-8 col-lg-6 center-block" style="float: none;">
<div class="panel panel-warning">
<div class="panel-heading"><h3 class="panel-title">计划任务说明</h3></div>
<div class="panel-body">
<p><li>计划任务将以下命令添加到计划任务1分钟1次</li></p>
<p><code>cd {:app()->getRootPath()} && php think certtask</code></p>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">自动续签设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-3 control-label">到期前续签天数</label>
<div class="col-sm-9"><input type="text" name="cert_renewdays" value="{:config_get('cert_renewdays', '7')}" class="form-control" placeholder="证书到期前多少天自动续签"/></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
</div>
</div>
</form>
</div>
<div class="panel-footer">
<li>提示:只有已开启自动续签的证书,才会自动续签。</li>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">自动部署设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-3 control-label">部署任务运行时段(小时)</label>
<div class="col-sm-9"><div class="input-group"><select class="form-control" name="deploy_hour_start" default="{:config_get('deploy_hour_start', '0')}">{for start="0" end="24"}<option value="{$i}">{$i}</option>{/for}</select><span class="input-group-addon"></span><select class="form-control" name="deploy_hour_end" default="{:config_get('deploy_hour_end', '23')}">{for start="0" end="24"}<option value="{$i}">{$i}</option>{/for}</select></div></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
</div>
</div>
</form>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">通知设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-3 control-label">邮件通知</label>
<div class="col-sm-9"><select class="form-control" name="cert_notice_mail" default="{:config_get('cert_notice_mail')}"><option value="0">关闭</option><option value="1">开启</option><option value="2">开启(仅失败时)</option></select></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">微信公众号通知</label>
<div class="col-sm-9"><select class="form-control" name="cert_notice_wxtpl" default="{:config_get('cert_notice_wxtpl')}"><option value="0">关闭</option><option value="1">开启</option><option value="2">开启(仅失败时)</option></select></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Telegram机器人通知</label>
<div class="col-sm-9"><select class="form-control" name="cert_notice_tgbot" default="{:config_get('cert_notice_tgbot')}"><option value="0">关闭</option><option value="1">开启</option><option value="2">开启(仅失败时)</option></select></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9"><input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/></div>
</div>
</form>
</div>
<div class="panel-footer">
<li>开启后,将在证书签发成功/失败,自动部署任务成功/失败时发送通知。只有计划任务执行会发送通知,控制台手动执行的不会发送。</li>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script>
var items = $("select[default]");
for (i = 0; i < items.length; i++) {
$(items[i]).val($(items[i]).attr("default")||0);
}
function saveSetting(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '',
data : $(obj).serialize(),
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert('设置保存成功!', {
icon: 1,
closeBtn: false
}, function(){
window.location.reload()
});
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
return false;
}
</script>
{/block}

244
app/view/cert/cname.html Normal file
View File

@ -0,0 +1,244 @@
{extend name="common/layout" /}
{block name="title"}CMAME代理记录管理{/block}
{block name="main"}
<style>
.copy-btn{color:#52c41a;cursor:pointer;margin-right: 5px;}
.copy-btn:hover{color:#85ef79;}
tbody tr>td:nth-child(3){word-break:break-all;max-width:180px;}
tbody tr>td:nth-child(4){word-break:break-all;max-width:260px;}
</style>
<div class="modal" id="modal-store" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content animated flipInX">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span
aria-hidden="true">&times;</span><span
class="sr-only">Close</span></button>
<h4 class="modal-title" id="modal-title">添加CNAME代理</h4>
</div>
<div class="modal-body">
<form class="form-horizontal" id="form-store">
<input type="hidden" name="action"/>
<input type="hidden" name="id"/>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">被代理域名</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="domain" onchange="changeDomain(this)" placeholder="需要申请SSL证书但未在本系统添加的域名" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">CNAME记录值</label>
<div class="col-sm-9"><div class="input-group">
<input type="text" name="rr" placeholder="自定义主机记录" class="form-control" required><span class="input-group-addon">.</span>
<select name="did" class="form-control" required>
{foreach $domains as $k=>$v}
<option value="{$k}">{$v}</option>
{/foreach}
</select></div></div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="store" onclick="save()">保存</button>
</div>
</div>
</div>
</div>
<div class="panel panel-default"><div class="panel-body"><p>CNAME代理可以让未在本系统添加的域名自动申请SSL证书支持所有DNS服务商。</p><p>仅支持基于ACME的证书类型不支持腾讯云等云厂商SSL证书。</p></div></div>
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default panel-intro">
<div class="panel-body">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="kw" placeholder="被代理域名">
</div>
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
<a href="javascript:searchClear()" class="btn btn-default" title="刷新列表"><i class="fa fa-refresh"></i> 刷新</a>
<a href="javascript:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
</form>
<table id="listTable">
</table>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}clipboard.js/1.7.1/clipboard.min.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/bootstrap-table.min.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 15;
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
$("#listTable").bootstrapTable({
url: '/cert/cname/data',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bordered',
uniqueId: 'id',
columns: [
{
field: 'id',
title: 'ID'
},
{
field: 'domain',
title: '被代理域名'
},
{
field: 'host',
title: '主机记录',
formatter: function(value, row, index) {
return value + '<a href="javascript:;" data-clipboard-text="'+value+'" class="copy-btn pull-right"><i class="fa fa-copy"></i></a>';
}
},
{
field: 'record',
title: 'CNAME记录值',
formatter: function(value, row, index) {
return value + '<a href="javascript:;" data-clipboard-text="'+value+'" class="copy-btn pull-right"><i class="fa fa-copy"></i></a>';
}
},
{
field: 'status',
title: '状态',
formatter: function(value, row, index) {
var html = '';
if(value == 1) {
html += '<span class="label label-success">已验证</span>';
} else {
html += '<span class="label label-warning">未验证</span>';
}
html += '&nbsp;<a href="javascript:checkItem('+row.id+')" title="立即验证" class="btn btn-primary btn-xs"><i class="fa fa-refresh"></i></a>';
return html;
}
},
{
field: 'addtime',
title: '添加时间'
},
{
field: '',
title: '操作',
formatter: function(value, row, index) {
var html = '<a href="javascript:editframe('+row.id+')" class="btn btn-primary btn-xs">编辑</a> <a href="javascript:delItem('+row.id+')" class="btn btn-danger btn-xs">删除</a>';
return html;
}
},
],
onLoadSuccess: function(data){
var clipboard = new Clipboard('.copy-btn');
clipboard.on('success', function (e) {
layer.msg('复制成功!', {icon: 1, time: 600});
});
clipboard.on('error', function (e) {
layer.msg('复制失败', {icon: 2});
});
},
})
})
function addframe(){
$("#modal-store").modal('show');
$("#modal-title").html("添加CNAME代理");
$("#form-store input[name=action]").val("add");
$("#form-store input[name=id]").val('');
$("#form-store input[name=domain]").val('');
$("#form-store input[name=domain]").prop('readonly', false);
$("#form-store input[name=rr]").val('');
var defaultDid = getCookie('cname_did');
if(defaultDid){
$("#form-store select[name=did]").val(defaultDid);
}
}
function editframe(id){
var row = $("#listTable").bootstrapTable('getRowByUniqueId', id);
$("#modal-store").modal('show');
$("#modal-title").html("修改CNAME代理");
$("#form-store input[name=action]").val("edit");
$("#form-store input[name=id]").val(id);
$("#form-store input[name=domain]").val(row.domain);
$("#form-store input[name=domain]").prop('readonly', true);
$("#form-store input[name=rr]").val(row.rr);
$("#form-store select[name=did]").val(row.did);
}
function changeDomain(obj){
var domain = $(obj).val();
if(domain == '') {
$("#form-store input[name=rr]").val('');
return;
}
var rr = domain.replace(/\./g, '-') + '.cname';
$("#form-store input[name=rr]").val(rr);
}
function save(){
if($("#form-store input[name=domain]").val()=='' || $("#form-store input[name=rr]").val()==''){
layer.alert('请确保各项不能为空!');return false;
}
var act = $("#form-store input[name=action]").val();
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/cert/cname/'+act,
data : $("#form-store").serialize(),
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
setCookie('cname_did', $("#form-store select[name=did]").val(), 2562000);
layer.alert(data.msg,{
icon: 1,
closeBtn: false
}, function(){
layer.closeAll();
$("#modal-store").modal('hide');
searchRefresh();
});
}else{
layer.alert(data.msg, {icon: 2})
}
}
});
}
function delItem(id){
layer.confirm('确定要删除此CNAME代理记录吗', {
btn: ['确定','取消']
}, function(){
$.post('/cert/cname/del', {id: id}, function(data){
if(data.code == 0) {
layer.msg('删除成功', {icon: 1, time:800});
searchRefresh();
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
});
}
function checkItem(id){
var ii = layer.load(2);
$.post('/cert/cname/check', {id: id}, function(data){
layer.close(ii);
if(data.code == 0) {
if(data.status == 1){
layer.alert('验证已通过!', {icon: 6});
}else{
layer.alert('验证未通过请按要求添加CNAME解析', {icon: 5});
}
searchRefresh();
} else {
layer.alert(data.msg, {icon: 2});
}
}, 'json');
}
</script>
{/block}

View File

@ -0,0 +1,253 @@
{extend name="common/layout" /}
{block name="title"}自动部署任务{/block}
{block name="main"}
<style>
.tips{color: #f6a838; padding-left: 5px;}
.input-note{color: green;}
.control-label[is-required]:before {
content: "*";
color: #f56c6c;
margin-right: 4px;
}
</style>
<div class="row" id="app">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title"><a href="/cert/deploytask" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>{if $action=='edit'}编辑{else}添加{/if}自动部署任务</h3></div>
<div class="panel-body">
<form onsubmit="return false" method="post" class="form-horizontal" role="form" id="accountform">
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" is-required>自动部署账户</label>
<div class="col-sm-6"><select name="aid" v-model="set.aid" class="form-control" required :disabled="action=='edit'">
<option value="">--选择自动部署账户--</option>
{foreach $accounts as $k=>$v}
<option value="{$k}" data-type="{$v.type}">{$v.name}</option>
{/foreach}
</select></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" is-required>关联SSL证书</label>
<div class="col-sm-6"><select name="oid" v-model="set.oid" class="form-control select2" placeholder="选择要部署的SSL证书">
{foreach $orders as $k=>$v}
<option value="{$k}">{$v.name}</option>
{/foreach}
</select></div>
</div>
<div v-for="(item,name) in inputs" v-show="isShow(item.show)">
<div class="form-group" v-if="item.type=='input'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<input type="text" class="form-control" :name="name" v-model="config[name]" :placeholder="item.placeholder" :required="item.required" :disabled="item.disabled" :data-bv-id="item.validator=='id'" :data-bv-phone="item.validator=='phone'" :data-bv-numeric="item.validator=='numeric'" :data-bv-digits="item.validator=='digits'" :data-bv-integer="item.validator=='integer'" :data-bv-email="item.validator=='email'" :data-bv-uri="item.validator=='uri'" :min="item.min" :max="item.max"><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='textarea'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<textarea class="form-control" :name="name" rows="5" v-model="config[name]" :placeholder="item.placeholder" :required="item.required" :disabled="item.disabled"></textarea><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='select'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<select class="form-control" :name="name" v-model="config[name]" :required="item.required" :disabled="item.disabled" :placeholder="item.placeholder">
<option v-for="option in item.options" :value="option.value">{{option.label}}</option>
</select><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='radio'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<label class="radio-inline" v-for="(optionname, optionvalue) in item.options">
<input type="radio" :name="name" :value="optionvalue" v-model="config[name]" :disabled="item.disabled"> {{optionname}}
</label><br/><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
<div class="form-group" v-if="item.type=='checkbox'">
<div class="col-sm-offset-3 col-sm-7">
<div class="checkbox">
<label>
<input type="checkbox" :name="name" v-model="config[name]" :disabled="item.disabled"> {{item.name}}
</label>
</div>
</div>
</div>
<div class="form-group" v-if="item.type=='checkboxes'">
<label class="col-sm-3 control-label no-padding-right" :is-required="item.required">{{item.name}}</label>
<div class="col-sm-6">
<label class="checkbox-inline" v-for="(optionname, optionvalue) in item.options">
<input type="checkbox" :name="name" :value="optionvalue" v-model="config[name]" :disabled="item.disabled"> {{optionname}}
</label><br/><span v-if="item.note" class="input-note" v-html="item.note"></span>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">备注</label>
<div class="col-sm-6">
<input type="text" name="remark" v-model="set.remark" placeholder="可留空" class="form-control">
</div>
</div>
<div class="form-group" v-show="note">
<div class="col-sm-offset-3 col-sm-6">
<div class="alert alert-dismissible alert-info">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<strong>提示:</strong><span v-html="note"></span>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-6"><button type="button" class="btn btn-primary" @click="submit">提交</button></div>
</div>
</form>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}select2/4.0.13/js/select2.min.js"></script>
<script src="{$cdnpublic}select2/4.0.13/js/i18n/zh-CN.min.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>
var info = {$info|json_encode|raw};
var typeList = {$typeList|json_encode|raw};
new Vue({
el: '#app',
data: {
action: '{$action}',
set: {
id: '',
aid: '',
oid: '',
config : '',
remark: '',
type: '',
},
inputs: {},
config: {},
typeList: typeList,
note: '',
},
watch: {
'set.aid': function(val){
this.set.type = $('option:selected', 'select[name=aid]').data('type');
},
'set.type': function(val){
if(this.action == 'add' && val && typeList[val]){
this.inputs = typeList[val].taskinputs;
this.note = typeList[val].tasknote;
this.config = {};
$.each(this.inputs, (name, item) => {
if(typeof item.value == 'undefined'){
if(item.type == 'checkbox'){
item.value = false;
}else if(item.type == 'checkboxes'){
item.value = [];
}else{
item.value = null;
}
}
this.$set(this.config, name, item.value)
})
}
}
},
mounted() {
if(this.action == 'edit'){
Object.keys(info).forEach((key) => {
this.set[key] = info[key]
})
var config = JSON.parse(info.config);
this.inputs = typeList[this.set.type].taskinputs;
this.note = typeList[this.set.type].tasknote;
$.each(this.inputs, (name, item) => {
item.value = config[name];
if(typeof item.value == 'undefined'){
if(item.type == 'checkbox'){
item.value = false;
}else if(item.type == 'checkboxes'){
item.value = [];
}else{
item.value = null;
}
}
this.$set(this.config, name, item.value)
})
}
var that = this;
this.$nextTick(function () {
$('[data-toggle="tooltip"]').tooltip();
$('select[name=oid]').select2({placeholder: '选择要部署的SSL证书'}).on('select2:select', function(e){
that.set.oid = e.params.data.id
});
if(document.referrer.indexOf('&oid=') > 0){
var oid = document.referrer.split('&oid=')[1].split('&')[0];
if(oid){
$('select[name=oid]').val(20).trigger('change');
that.set.oid = oid;
}
}
})
},
methods: {
submit(){
var that=this;
if(this.set.aid == ''){
layer.msg('请选择自动部署账户', {icon: 2, time:900});
return false;
}
if(this.set.oid == ''){
layer.msg('请选择要部署的SSL证书', {icon: 2, time:900});
return false;
}
Object.keys(this.config).forEach((key) => {
if(this.config[key] && typeof this.config[key] == 'string'){
this.config[key] = this.trim(this.config[key]);
}
})
this.set.config = JSON.stringify(this.config);
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type: "POST",
url: "",
data: this.set,
dataType: 'json',
success: function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg, {icon: 1}, function(){
if(document.referrer.indexOf('/cert/deploytask?') > 0)
window.location.href = document.referrer;
else
window.location.href = '/cert/deploytask';
});
}else{
layer.alert(data.msg, {icon: 2});
}
},
error: function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
},
isShow(show){
if(typeof show == 'boolean' && show){
return show;
}else if(typeof show == 'string' && show){
var that=this;
Object.keys(this.config).forEach((key) => {
show = show.replace(new RegExp(key, 'g'), 'that.config["'+key+'"]')
})
return eval(show);
}else{
return true;
}
},
trim(str){
return str.replace(/(^\s*)|(\s*$)/g, "");
}
},
});
</script>
{/block}

View File

@ -0,0 +1,93 @@
{extend name="common/layout" /}
{block name="title"}自动部署任务账户管理{/block}
{block name="main"}
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default panel-intro">
<div class="panel-body">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="kw" placeholder="账户名称或备注">
</div>
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
<a href="javascript:searchClear()" class="btn btn-default" title="刷新SSL证书账户列表"><i class="fa fa-refresh"></i> 刷新</a>
<a href="/cert/account/add?deploy=1" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
</form>
<table id="listTable">
</table>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/bootstrap-table.min.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 15;
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
$("#listTable").bootstrapTable({
url: '/cert/account/data?deploy=1',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bordered',
columns: [
{
field: 'id',
title: 'ID'
},
{
field: 'typename',
title: '账户类型',
formatter: function(value, row, index) {
return '<img src="/static/images/'+row.icon+'" class="type-logo"></img>'+value;
}
},
{
field: 'name',
title: '账户名称'
},
{
field: 'remark',
title: '备注'
},
{
field: 'addtime',
title: '添加时间'
},
{
field: '',
title: '操作',
formatter: function(value, row, index) {
var html = '<a href="/cert/account/edit?deploy=1&id='+row.id+'" class="btn btn-info btn-xs">编辑</a> <a href="javascript:delItem('+row.id+')" class="btn btn-danger btn-xs">删除</a>';
return html;
}
},
]
})
})
function delItem(id){
layer.confirm('确定要删除此账户吗?', {
btn: ['确定','取消']
}, function(){
$.post('/cert/account/del?deploy=1', {id: id}, function(data){
if(data.code == 0) {
layer.msg('删除成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
});
}
</script>
{/block}

View File

@ -0,0 +1,286 @@
{extend name="common/layout" /}
{block name="title"}SSL证书自动部署任务{/block}
{block name="main"}
<style>
tbody tr>td:nth-child(3){max-width:180px;}
.tips{cursor:pointer;}
pre.pre-log{height: 330px;overflow-y: auto;width: 100%;background-color: rgba(51, 51, 51, 1);white-space: pre-line;color: rgba(236, 236, 236, 1)}
</style>
<div class="row">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default panel-intro">
<div class="panel-body">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<input type="hidden" name="oid" value="">
<div class="form-group">
<label>搜索</label>
<div class="form-group">
<input type="text" class="form-control" name="domain" placeholder="域名">
</div>
</div>
<div class="form-group">
<div class="form-group">
<input type="text" class="form-control" name="remark" placeholder="备注">
</div>
</div>
<div class="form-group">
<select name="type" class="form-control"><option value="">所有平台</option>{foreach $types as $k=>$v}
<option value="{$k}">{$v}</option>
{/foreach}</select>
</div>
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
<a href="javascript:searchClear()" class="btn btn-default" title="刷新任务列表"><i class="fa fa-refresh"></i> 刷新</a>
<a href="/cert/deploy/add" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
</form>
<table id="listTable">
</table>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/bootstrap-table.min.js"></script>
<script src="{$cdnpublic}bootstrap-table/1.21.4/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="{$cdnpublic}FileSaver.js/2.0.5/FileSaver.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 10;
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
$("#listTable").bootstrapTable({
url: '/cert/deploy/data',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bordered',
uniqueId: 'id',
columns: [
{
field: 'id',
title: 'ID'
},
{
field: 'typename',
title: '自动部署账户',
formatter: function(value, row, index) {
return '<span title="'+row.aname+'" data-toggle="tooltip" data-placement="right"><img src="/static/images/'+row.icon+'" class="type-logo">'+(row.aremark?row.aremark:value+'('+row.aid+')')+'</span>';
}
},
{
field: 'domains',
title: '关联SSL证书',
formatter: function(value, row, index) {
var html = '<a href="/cert/certorder?id='+row.oid+'" target="_blank">ID:'+row.oid+''+row.certtypename+'</a><br/><span class="text-muted">';
html += value.length > 3 ? value.slice(0, 3).join('、') + ' 等'+value.length+'个域名' : value.join('、');
html += '</span>';
return html;
}
},
{
field: 'remark',
title: '备注'
},
{
field: 'active',
title: '任务开关',
formatter: function(value, row, index) {
if(value == 1){
return '<div class="material-switch"><input id="active'+row.id+'" type="checkbox" checked onchange="setActive('+row.id+',0)"/><label for="active'+row.id+'" class="label-primary"></label></div>';
}else{
return '<div class="material-switch"><input id="active'+row.id+'" type="checkbox" onchange="setActive('+row.id+',1)"/><label for="active'+row.id+'" class="label-primary"></label></div>';
}
}
},
{
field: 'lasttime',
title: '上次执行时间',
formatter: function(value, row, index) {
return value ? value : '暂未执行'
}
},
{
field: 'status',
title: '状态',
formatter: function(value, row, index) {
if(value == 1) {
return '<span class="label label-success">已完成</span>';
} else if(value == 0) {
if(row.islock == 1) return '<span class="label" style="background-color: #3e76fb;">正在处理</span>';
else return '<span class="label label-info">待处理</span>';
} else {
return '<span class="label label-danger">处理失败</span>'+(row.error?' <span onclick="showmsg(\''+row.error+'\')" class="tips" title="失败原因"><i class="fa fa-info-circle"></i></span>':'');
}
}
},
{
field: '',
title: '操作',
formatter: function(value, row, index) {
var html = '';
if(row.status == 0) {
html += '<a href="javascript:doOrder(\''+row.id+'\')" class="btn btn-success btn-xs"><i class="fa fa-play-circle"></i> 手动执行</a>&nbsp;&nbsp;';
}else if(row.status == 1) {
html += '<a href="javascript:reDoOrder(\''+row.id+'\')" class="btn btn-success btn-xs"><i class="fa fa-play-circle"></i> 重新执行</a>&nbsp;&nbsp;';
}else{
html += '<a href="javascript:doOrder(\''+row.id+'\')" class="btn btn-success btn-xs"><i class="fa fa-repeat"></i> 重试</a>&nbsp;&nbsp;';
}
html += '<a href="/cert/deploy/edit?id='+row.id+'" class="btn btn-primary btn-xs"><i class="fa fa-edit"></i> 修改</a>&nbsp;&nbsp;';
html += '<div class="btn-group dropdown-group" role="group"><button type="button" class="btn btn-info btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">更多 <span class="caret"></span></button><ul class="dropdown-menu">';
html += '<li><a href="javascript:showLog(\''+row.processid+'\')">查看日志</a></li>';
if(row.status < 0){
html += '<li><a href="javascript:resetOrder(\''+row.id+'\')">重置任务</a></li>';
}
html += '<li><a href="javascript:delItem('+row.id+','+row.status+')">删除</a></li>';
html += '</ul></div>';
return html;
}
},
],
onLoadSuccess: function(data) {
$('[data-toggle="tooltip"]').tooltip()
$('.dropdown-group').on('show.bs.dropdown', function (e) {
var btnPos = $(e.target)[0].getBoundingClientRect();
var screenWidth = $(window).width();
var screenHeight = $(window).height();
var childrenWidth = $(e.target).children('.dropdown-menu').width();
var childrenHeight = $(e.target).children('.dropdown-menu').height();
var top = btnPos.bottom;
if(top + childrenHeight + 12 > screenHeight){
top = btnPos.top - childrenHeight - 12;
}
var left = btnPos.left;
if(left + childrenWidth + 7 > screenWidth){
left = screenWidth - childrenWidth - 7;
}
$(e.target).children('.dropdown-menu').css({position:'fixed', top:top, left:left});
});
}
})
})
function showmsg(msg){
layer.alert(msg, {icon: 0, title: '失败原因'});
}
function setActive(id, active){
$.post('/cert/deploy/setactive', {id: id, active: active}, function(data){
if(data.code == 0) {
layer.msg('修改成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
}
function delItem(id,status){
layer.confirm('确定要删除此自动部署任务吗?', {
btn: ['确定','取消']
}, function(){
$.post('/cert/deploy/del', {id: id}, function(data){
if(data.code == 0) {
layer.msg('删除成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
});
}
function doOrder(id, reset){
reset = reset || 0;
var ii = layer.msg('正在执行SSL证书部署...', {icon: 16,shade: 0.1,time: 0});
$.ajax({
type: "POST",
url: "/cert/deploy/process",
data: {id: id, reset: reset},
dataType: 'json',
success: function(data) {
layer.close(ii);
if(data.code == 0) {
layer.alert(data.msg, {icon: 1});
$('#listTable').bootstrapTable('refresh');
} else {
if(data.msg == '部署任务处理中,请稍后再试'){
layer.alert(data.msg, {icon: 2}, function(){
layer.closeAll();
var row = $("#listTable").bootstrapTable('getRowByUniqueId', id);
showLog(row.processid)
});
}else{
layer.alert(data.msg, {icon: 2});
$('#listTable').bootstrapTable('refresh');
}
}
},
error: function(data){
layer.close(ii);
layer.msg('执行超时,请稍后刷新列表或查看日志', {icon:0});
}
});
}
function resetOrder(id){
layer.confirm('重置任务后,任务将变成待处理状态,是否确定?', {
btn: ['确定','取消'], title: '重置任务', icon: 0
}, function(){
$.post('/cert/deploy/reset', {id: id}, function(data){
if(data.code == 0) {
layer.msg('重置任务状态成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
}
}, 'json');
});
}
function reDoOrder(id){
layer.confirm('是否确定重新部署该证书?', {
btn: ['确定','取消'], title: '重新执行', icon: 0
}, function(){
doOrder(id, 1);
});
}
var intverval;
function showLog(processid){
if(processid == '' || processid == 'null'){
layer.msg('暂无日志', {time: 600});
return;
}
$.post('/cert/deploy/show_log', {processid: processid}, function(data){
if(data.code == 0) {
var filemtime = data.time;
layer.open({
type: 1,
title: '查看日志',
area: [$(window).width() > 768 ? '600px' : '100%', '400px'],
shadeClose: true,
resize: false,
content: '<div class="modal-log"><pre class="pre-log" id="execLog">'+data.data+'</pre></div>',
success: function(){
var exec_log = $('#execLog');
exec_log[0].scrollTop = exec_log[0].scrollHeight
intverval = setInterval(function(){
$.post('/cert/deploy/show_log', {processid: processid}, function(data){
if(data.code == 0 && data.time != filemtime) {
var exec_log = $('#execLog');
exec_log.html(data.data);
filemtime = data.time;
exec_log[0].scrollTop = exec_log[0].scrollHeight
}
}, 'json');
}, 1500)
},
end: function(){
clearInterval(intverval);
}
});
} else {
layer.msg(data.msg, {icon: 2, time: 600});
}
}, 'json');
}
</script>
{/block}

View File

@ -0,0 +1,157 @@
{extend name="common/layout" /}
{block name="title"}SSL证书订单{/block}
{block name="main"}
<style>
.tips{color: #f6a838; padding-left: 5px;}
.control-label[is-required]:before {
content: "*";
color: #f56c6c;
margin-right: 4px;
}
</style>
<div class="row" id="app">
<div class="col-xs-12 center-block" style="float: none;">
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title"><a href="/cert/certorder" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>{if $action=='edit'}修改{else}添加{/if}SSL证书订单</h3></div>
<div class="panel-body">
<form onsubmit="return false" method="post" class="form-horizontal" role="form" id="taskform">
<div class="form-group">
<label class="col-sm-3 col-xs-12 control-label no-padding-right" is-required>证书账户</label>
<div class="col-sm-6"><select name="aid" v-model="set.aid" class="form-control" required>
<option value="">--选择证书账户--</option>
{foreach $accounts as $k=>$v}
<option value="{$k}" data-type="{$v.type}">{$v.name}</option>
{/foreach}
</select></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" is-required>签名算法</label>
<div class="col-sm-6">
<label class="radio-inline" v-for="item in keytypeList">
<input type="radio" name="keytype" :value="item" v-model="set.keytype"> {{item}}
</label>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" is-required>密钥长度</label>
<div class="col-sm-6">
<label class="radio-inline" v-for="item in keysizeList">
<input type="radio" name="keysize" :value="item.value" v-model="set.keysize"> {{item.label}}
</label>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" is-required>绑定域名</label>
<div class="col-sm-6">
<textarea name="domains" v-model="domains" class="form-control" rows="5" placeholder="请输入域名,一行一个" required></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-6"><button type="button" class="btn btn-primary" @click="submit">提交</button></div>
</div>
<div class="panel panel-default"><div class="panel-body"><p><b style="color:#39b603;"><i class="fa fa-info-circle fa-fw"></i></b>提示:添加或修改订单信息,点击提交后,不会立即执行签发,只能通过计划任务或列表手动点击来执行</p><p>证书签发之前确保该主域名下没有CAA类型记录避免证书验证失败。</p></div></div>
</form>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}vue/2.6.14/vue.min.js"></script>
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>
var action = '{$action}';
var info = {$info|json_encode|raw};
new Vue({
el: '#app',
data: {
action: '{$action}',
type: '',
domains: '',
set: {
id: '',
aid: '',
keytype: '',
keysize: '',
domains: [],
},
keytypeList: [
'RSA',
'ECC'
],
keysizeMap: [
{label:'2048 bit',value:'2048',type:'RSA'},
{label:'3072 bit',value:'3072',type:'RSA'},
{label:'P-256',value:'256',type:'ECC'},
{label:'P-384',value:'384',type:'ECC'},
],
keysizeList: [],
},
watch: {
'set.aid': function(val){
this.type = $('option:selected', 'select[name=aid]').data('type');
},
'set.keytype': function(val){
this.keysizeList = this.keysizeMap.filter((item) => {
return item.type == val;
})
if(!this.keysizeList.filter((item) => {return item.value == this.set.keysize}).length)
this.set.keysize = this.keysizeList[0].value;
},
domains: function(val){
this.set.domains = val.split("\n").filter((item) => {
return item.trim() != '';
});
}
},
mounted() {
if(this.action == 'edit'){
Object.keys(info).forEach((key) => {
this.$set(this.set, key, info[key])
})
this.domains = info.domains.join("\n");
}else{
this.set.keytype = 'RSA';
}
$("#taskform").bootstrapValidator({
live: 'submitted',
});
$('[data-toggle="tooltip"]').tooltip();
},
methods: {
submit(){
var that=this;
$("#taskform").data("bootstrapValidator").validate();
if(!$("#taskform").data("bootstrapValidator").isValid()){
return false;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type: "POST",
url: "",
data: this.set,
dataType: 'json',
success: function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg, {icon: 1}, function(){
if(document.referrer.indexOf('/cert/certorder?') > 0)
window.location.href = document.referrer;
else
window.location.href = '/cert/certorder';
});
}else{
layer.alert(data.msg, {icon: 2});
}
},
error: function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
}
},
});
</script>
{/block}

View File

@ -11,7 +11,7 @@
<link href="{$cdnpublic}select2/4.0.13/css/select2.min.css" rel="stylesheet"/>
<link href="/static/css/app.min.css" rel="stylesheet">
<link href="/static/css/skins/{$skin}.css" rel="stylesheet">
<link href="/static/css/bootstrap-table.css" rel="stylesheet"/>
<link href="/static/css/bootstrap-table.css?v=2" rel="stylesheet"/>
<script src="{$cdnpublic}jquery/3.6.4/jquery.min.js"></script>
<!--[if lt IE 9]>
<script src="{$cdnpublic}html5shiv/3.7.3/html5shiv.min.js"></script>
@ -107,7 +107,10 @@
<a href="/domain"><i class="fa fa-list-ul fa-fw"></i> <span>域名管理</span></a>
</li>
{if request()->user['level'] eq 2}
<li class="treeview {:checkIfActive('overview,task,noticeset,taskinfo,taskform,proxyset')}">
<li class="{:checkIfActive('account')}">
<a href="/account"><i class="fa fa-lock fa-fw"></i> <span>域名账户</span></a>
</li>
<li class="treeview {:checkIfActive('overview,task,taskinfo,taskform')}">
<a href="javascript:;">
<i class="fa fa-heartbeat fa-fw"></i>
<span>容灾切换</span>
@ -118,8 +121,6 @@
<ul class="treeview-menu">
<li><a href="/dmonitor/overview"><i class="fa fa-circle-o"></i> 运行概览</a></li>
<li><a href="/dmonitor/task"><i class="fa fa-circle-o"></i> 切换策略</a></li>
<li><a href="/dmonitor/noticeset"><i class="fa fa-circle-o"></i> 通知设置</a></li>
<li><a href="/dmonitor/proxyset"><i class="fa fa-circle-o"></i> 代理设置</a></li>
</ul>
</li>
<li class="treeview {:checkIfActive('opipset,opiplist,opipform')}">
@ -135,8 +136,36 @@
<li><a href="/optimizeip/opiplist"><i class="fa fa-circle-o"></i> 任务管理</a></li>
</ul>
</li>
<li class="{:checkIfActive('account')}">
<a href="/account"><i class="fa fa-lock fa-fw"></i> <span>域名账户</span></a>
<li class="treeview {:checkIfActive('certaccount,account_form,certorder,order_form,deployaccount,deploytask,deploy_form,certset,cname')}">
<a href="javascript:;">
<i class="fa fa-expeditedssl fa-fw"></i>
<span>SSL证书</span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu">
<li><a href="/cert/certaccount"><i class="fa fa-circle-o"></i> SSL证书账户</a></li>
<li><a href="/cert/certorder"><i class="fa fa-circle-o"></i> SSL证书订单</a></li>
<li><a href="/cert/deployaccount"><i class="fa fa-circle-o"></i> 自动部署账户</a></li>
<li><a href="/cert/deploytask"><i class="fa fa-circle-o"></i> 自动部署任务</a></li>
<li><a href="/cert/cname"><i class="fa fa-circle-o"></i> CNAME代理</a></li>
<li><a href="/cert/certset"><i class="fa fa-circle-o"></i> 计划任务设置</a></li>
</ul>
</li>
<li class="treeview {:checkIfActive('noticeset,proxyset')}">
<a href="javascript:;">
<i class="fa fa-cogs fa-fw"></i>
<span>系统设置</span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu">
<li><a href="/system/noticeset"><i class="fa fa-circle-o"></i> 通知设置</a></li>
<li><a href="/system/proxyset"><i class="fa fa-circle-o"></i> 代理设置</a></li>
<li><a href="https://www.showdoc.com.cn/dnsmgr/11058996709621562" target="_blank" rel="noreferrer"><i class="fa fa-circle-o"></i> <span>接口文档</span></a></li>
</ul>
</li>
<li class="{:checkIfActive('user')}">
<a href="/user"><i class="fa fa-user fa-fw"></i> <span>用户管理</span></a>
@ -144,11 +173,6 @@
<li class="{:checkIfActive('log')}">
<a href="/log"><i class="fa fa-list fa-fw"></i> <span>操作日志</span></a>
</li>
{if request()->user['type'] eq 'user'}
<li class="">
<a href="https://www.showdoc.com.cn/dnsmgr/11058996709621562" target="_blank"><i class="fa fa-book fa-fw"></i> <span>接口文档</span></a>
</li>
{/if}
</ul>
</section>
<!-- /.sidebar -->
@ -202,8 +226,8 @@
</div>
<!-- ./wrapper -->
<script src="<?php echo $cdnpublic?>twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script src="<?php echo $cdnpublic?>fastclick/1.0.6/fastclick.min.js"></script>
<script src="{$cdnpublic}twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script src="{$cdnpublic}fastclick/1.0.6/fastclick.min.js"></script>
<script src="/static/js/app.js"></script>
<script>
document.addEventListener('pointerdown', function(e) {

View File

@ -33,6 +33,38 @@
</div>
</div>
</div>
<div class="modal" id="modal-notice">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">通知设置</h4>
</div>
<div class="modal-body">
<form id="form-notice" onsubmit="return false;" class="form-horizontal">
<div class="form-group">
<label class="col-sm-4 control-label">邮件通知</label>
<div class="col-sm-8"><select class="form-control" name="notice_mail" default="{:config_get('notice_mail')}"><option value="0">关闭</option><option value="1">开启</option></select></div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">微信公众号通知</label>
<div class="col-sm-8"><select class="form-control" name="notice_wxtpl" default="{:config_get('notice_wxtpl')}"><option value="0">关闭</option><option value="1">开启</option></select></div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Telegram机器人通知</label>
<div class="col-sm-8"><select class="form-control" name="notice_tgbot" default="{:config_get('notice_tgbot')}"><option value="0">关闭</option><option value="1">开启</option></select></div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-info" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="submitNotice()">确定</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3 col-sm-6 col-xs-12">
<div class="info-box">
@ -105,7 +137,7 @@
<i class="fa fa-list-alt fa-2x"></i>
<span>切换策略</span>
</a>
<a href="/dmonitor/noticeset" class="col">
<a href="javascript:noticeset()" class="col">
<i class="fa fa-bullhorn fa-2x"></i>
<span>通知设置</span>
</a>
@ -133,9 +165,16 @@
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script>
var items = $("select[default]");
for (i = 0; i < items.length; i++) {
$(items[i]).val($(items[i]).attr("default")||0);
}
function clean(){
$('#modal-clean').modal('show');
}
function noticeset(){
$('#modal-notice').modal('show');
}
function submitClean(){
var days = $('#form-clean input[name=days]').val();
if(days < 1){
@ -151,5 +190,19 @@ function submitClean(){
}
});
}
function submitNotice(){
$.post('/dmonitor/noticeset', $("#form-notice").serialize(), function(res){
if(res.code == 0){
layer.alert('设置保存成功!<br/>重启检测进程或容器后生效', {
icon: 1,
closeBtn: false
}, function(){
window.location.reload()
});
}else{
layer.alert(res.msg, {icon: 2});
}
});
}
</script>
{/block}

View File

@ -57,7 +57,7 @@ $(document).ready(function(){
field: 'rr',
title: '域名',
formatter: function(value, row, index) {
return '<span title="'+row.remark+'" data-toggle="tooltip" data-placement="right" title="Tooltip on right">' + value + '.' + row.domain + '</span>';
return '<span title="'+row.remark+'" data-toggle="tooltip" data-placement="right">' + value + '.' + row.domain + '</span>';
}
},
{
@ -114,13 +114,11 @@ $(document).ready(function(){
field: 'active',
title: '运行开关',
formatter: function(value, row, index) {
var html = '';
if(value == 1) {
html += '<span class="btn btn-success btn-xs" onclick="setActive('+row.id+', 0)">开启</span>';
} else {
html += '<span class="btn btn-warning btn-xs" onclick="setActive('+row.id+', 1)">暂停</span>';
if(value == 1){
return '<div class="material-switch"><input id="active'+row.id+'" type="checkbox" checked onchange="setActive('+row.id+',0)"/><label for="active'+row.id+'" class="label-primary"></label></div>';
}else{
return '<div class="material-switch"><input id="active'+row.id+'" type="checkbox" onchange="setActive('+row.id+',1)"/><label for="active'+row.id+'" class="label-primary"></label></div>';
}
return html;
}
},
{

View File

@ -25,10 +25,58 @@
</form>
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading"><h3 class="panel-title">TOTP二次验证</h3></div>
<div class="panel-body">
<form onsubmit="return saveAccount(this)" method="post" class="form" role="form">
<div class="form-group">
<div class="input-group">
{if $user.totp_open == 1}
<input type="text" name="totp_status" value="已开启" style="color:green" class="form-control" readonly/>
<div class="input-group-btn"><button type="button" class="btn btn-info" onclick="open_totp()">重置</button></div>
<div class="input-group-btn"><button type="button" class="btn btn-danger" onclick="close_totp()">关闭</button></div>
{else}
<input type="text" name="totp_status" value="未开启" style="color:blue" class="form-control" readonly/>
<div class="input-group-btn"><button type="button" class="btn btn-info" onclick="open_totp()">开启</button></div>
{/if}
</div>
</div>
</form>
</div>
<div class="panel-footer">
<p><span class="glyphicon glyphicon-info-sign"></span> 开启后登录时需使用支持TOTP的认证软件进行二次验证提高账号安全性。开启前需确保服务器时间正确。</p>
<p>支持TOTP的认证软件<a href="https://sj.qq.com/appdetail/com.tencent.authenticator" target="_blank" rel="noreferrer">腾讯身份验证器</a><a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank" rel="noreferrer">谷歌身份验证器</a><a href="https://www.microsoft.com/zh-cn/security/mobile-authenticator-app" target="_blank" rel="noreferrer">微软身份验证器</a><a href="https://github.com/freeotp" target="_blank" rel="noreferrer">FreeOTP</a></p>
</div>
</div>
<div class="modal" id="modal-totp" data-backdrop="static" data-keyboard="false" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">TOTP绑定</h4>
</div>
<div class="modal-body text-center">
<p>使用支持TOTP的认证软件扫描以下二维码</p>
<div class="qr-image mt-4" id="qrcode"></div>
<p><a href="javascript:;" data-clipboard-text="" id="copy-btn">复制密钥</a></p>
<form id="form-totp" style="text-align: left;" onsubmit="return bind_totp()">
<div class="form-group mt-4">
<div class="input-group"><input type="number" class="form-control input-lg" name="code" id="code" value="" placeholder="填写动态口令" autocomplete="off" required><div class="input-group-btn"><input type="submit" name="submit" value="完成绑定" class="btn btn-success btn-lg btn-block"/></div></div>
</div>
</form>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script src="{$cdnpublic}jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
<script src="{$cdnpublic}clipboard.js/1.7.1/clipboard.min.js"></script>
<script>
var commonData = {secret:null,qrcode:null};
function saveAccount(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
@ -56,5 +104,84 @@ function saveAccount(obj){
});
return false;
}
function open_totp(){
if(!commonData.qrcode || !commonData.secret){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.post('/totp/generate', {}, function(res){
layer.close(ii);
if(res.code == 0){
commonData.secret = res.data.secret;
commonData.qrcode = res.data.qrcode;
$('#qrcode').qrcode({
text: commonData.qrcode,
width: 150,
height: 150,
foreground: "#000000",
background: "#ffffff",
typeNumber: -1
});
$("#copy-btn").attr('data-clipboard-text', commonData.secret);
$('#modal-totp').modal('show');
$("#code").focus();
}else{
layer.alert(res.msg, {icon: 2});
}
});
}else{
$('#modal-totp').modal('show');
$("#code").focus();
}
}
function bind_totp(){
var code = $("#code").val();
if(code.length != 6){
layer.msg('动态口令格式错误', {icon: 2});
return false;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.post('/totp/bind', {secret:commonData.secret, code:code}, function(res){
layer.close(ii);
if(res.code == 0){
layer.alert('TOTP绑定成功', {icon: 1}, function(){
window.location.reload();
});
}else{
layer.alert(res.msg, {icon: 2});
}
});
return false;
}
function close_totp(){
layer.confirm('确定要关闭TOTP二次验证吗', {
btn: ['确定','取消']
}, function(){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.post('/totp/close', {}, function(res){
layer.close(ii);
if(res.code == 0){
layer.alert('TOTP已关闭', {icon: 1}, function(){
window.location.reload();
});
}else{
layer.alert(res.msg, {icon: 2});
}
});
});
}
$(document).ready(function(){
var clipboard = new Clipboard('#copy-btn');
clipboard.on('success', function (e) {
layer.msg('复制成功!', {icon: 1, time: 600});
});
clipboard.on('error', function (e) {
layer.msg('复制失败', {icon: 2});
});
$("#code").keyup(function(){
var code = $(this).val();
if(code.length == 6){
$("#form-totp").submit();
}
});
});
</script>
{/block}

View File

@ -57,7 +57,7 @@ $(document).ready(function(){
field: 'rr',
title: '域名',
formatter: function(value, row, index) {
return '<span title="'+row.remark+'" data-toggle="tooltip" data-placement="right" title="Tooltip on right">' + value + '.' + row.domain + '</span>';
return '<span title="'+row.remark+'" data-toggle="tooltip" data-placement="right">' + value + '.' + row.domain + '</span>';
}
},
{
@ -98,13 +98,11 @@ $(document).ready(function(){
field: 'active',
title: '任务开关',
formatter: function(value, row, index) {
var html = '';
if(value == 1) {
html += '<span class="btn btn-success btn-xs" onclick="setActive('+row.id+', 0)">开启</span>';
} else {
html += '<span class="btn btn-warning btn-xs" onclick="setActive('+row.id+', 1)">关闭</span>';
if(value == 1){
return '<div class="material-switch"><input id="active'+row.id+'" type="checkbox" checked onchange="setActive('+row.id+',0)"/><label for="active'+row.id+'" class="label-primary"></label></div>';
}else{
return '<div class="material-switch"><input id="active'+row.id+'" type="checkbox" onchange="setActive('+row.id+',1)"/><label for="active'+row.id+'" class="label-primary"></label></div>';
}
return html;
}
},
{

View File

@ -0,0 +1,200 @@
{extend name="common/layout" /}
{block name="title"}通知设置{/block}
{block name="main"}
<div class="row">
<div class="col-xs-12 col-sm-8 col-lg-6 center-block" style="float: none;">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">发信邮箱设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-3 control-label">发信模式</label>
<div class="col-sm-9"><select class="form-control" name="mail_type" default="{:config_get('mail_type')}"><option value="0">SMTP发信</option><option value="1">搜狐Sendcloud</option><option value="2">阿里云邮件推送</option></select></div>
</div>
<div id="frame_set1">
<div class="form-group">
<label class="col-sm-3 control-label">SMTP服务器</label>
<div class="col-sm-9"><input type="text" name="mail_smtp" value="{:config_get('mail_smtp')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">SMTP端口</label>
<div class="col-sm-9"><input type="text" name="mail_port" value="{:config_get('mail_port')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">邮箱账号</label>
<div class="col-sm-9"><input type="text" name="mail_name" value="{:config_get('mail_name')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">邮箱密码</label>
<div class="col-sm-9"><input type="text" name="mail_pwd" value="{:config_get('mail_pwd')}" class="form-control"/></div>
</div>
</div>
<div id="frame_set2">
<div class="form-group">
<label class="col-sm-3 control-label">API_USER</label>
<div class="col-sm-9"><input type="text" name="mail_apiuser" value="{:config_get('mail_apiuser')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">API_KEY</label>
<div class="col-sm-9"><input type="text" name="mail_apikey" value="{:config_get('mail_apikey')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">发信邮箱</label>
<div class="col-sm-9"><input type="text" name="mail_name2" value="{:config_get('mail_name')}" class="form-control"/></div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">收信邮箱</label>
<div class="col-sm-9"><input type="text" name="mail_recv" value="{:config_get('mail_recv')}" class="form-control" placeholder="不填默认为发信邮箱"/></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
<a href="javascript:mailtest()" class="btn btn-default btn-block">发送测试邮件</a>
</div>
</div>
</form>
</div>
<div class="panel-footer">
<span class="glyphicon glyphicon-info-sign"></span>
使用普通模式发信时建议使用QQ邮箱SMTP服务器smtp.qq.com端口465或587密码是QQ邮箱设置界面生成的<a href="https://service.mail.qq.com/detail/0/75" target="_blank" rel="noreferrer">授权码</a><br/>阿里云邮件推送:<a href="https://www.aliyun.com/product/directmail" target="_blank" rel="noreferrer">点此进入</a><a href="https://usercenter.console.aliyun.com/#/manage/ak" target="_blank" rel="noreferrer">获取AK/SK</a>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">微信公众号消息接口设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-3 control-label">appToken</label>
<div class="col-sm-9"><input type="text" name="wechat_apptoken" value="{:config_get('wechat_apptoken')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">用户UID</label>
<div class="col-sm-9"><input type="text" name="wechat_appuid" value="{:config_get('wechat_appuid')}" class="form-control"/></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9"><input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/></div>
</div>
</form>
</div>
<div class="panel-footer">
<b>WxPusher</b><a href="https://wxpusher.zjiecode.com/admin/" target="_blank" rel="noopener noreferrer">点此进入</a> ,注册并且创建应用 -> 将appToken填写到上方输入框 -> 扫码关注应用 -> 在用户列表查看自己的UID填写到上方输入框<br/>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">Telegram机器人接口设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-3 control-label">Token</label>
<div class="col-sm-9"><input type="text" name="tgbot_token" value="{:config_get('tgbot_token')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Chat Id</label>
<div class="col-sm-9"><input type="text" name="tgbot_chatid" value="{:config_get('tgbot_chatid')}" class="form-control"/></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">使用代理服务器</label>
<div class="col-sm-9"><select class="form-control" name="tgbot_proxy" default="{:config_get('tgbot_proxy')}"><option value="0"></option><option value="1"></option></select></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
<a href="javascript:tgbottest()" class="btn btn-default btn-block">发送测试消息</a>
</div>
</div>
</form>
</div>
<div class="panel-footer">
<a href="https://t.me/BotFather" target="_blank" rel="noopener noreferrer">@BotFather</a>对话,使用/newbot命令创建一个新的机器人根据提示输入机器人的名称和用户名可得到Token或使用/mybots命令查看已创建的机器人<a href="https://t.me/getmyid_bot" target="_blank" rel="noopener noreferrer">@getmyid_bot</a>对话可得到Chat Id<br/>
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script>
var items = $("select[default]");
for (i = 0; i < items.length; i++) {
$(items[i]).val($(items[i]).attr("default")||0);
}
$("select[name='mail_type']").change(function(){
if($(this).val() == 0){
$("#frame_set1").show();
$("#frame_set2").hide();
}else{
$("#frame_set1").hide();
$("#frame_set2").show();
}
});
$("select[name='mail_type']").change();
function saveSetting(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '',
data : $(obj).serialize(),
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert('设置保存成功!<br/>重启检测进程或容器后生效', {
icon: 1,
closeBtn: false
}, function(){
window.location.reload()
});
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
return false;
}
function mailtest(){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'GET',
url : '/system/mailtest',
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg, {icon: 1});
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
}
function tgbottest(){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'GET',
url : '/system/tgbottest',
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert(data.msg, {icon: 1});
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
}
</script>
{/block}

View File

@ -0,0 +1,113 @@
{extend name="common/layout" /}
{block name="title"}容灾切换代理设置{/block}
{block name="main"}
<div class="row">
<div class="col-xs-12 col-sm-8 col-lg-6 center-block" style="float: none;">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">代理服务器设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-3 control-label">代理IP</label>
<div class="col-sm-9"><input type="text" name="proxy_server" value="{:config_get('proxy_server')}" class="form-control"/></div>
</div><br/>
<div class="form-group">
<label class="col-sm-3 control-label">代理端口</label>
<div class="col-sm-9"><input type="text" name="proxy_port" value="{:config_get('proxy_port')}" class="form-control"/></div>
</div><br/>
<div class="form-group">
<label class="col-sm-3 control-label">代理账号</label>
<div class="col-sm-9"><input type="text" name="proxy_user" value="{:config_get('proxy_user')}" class="form-control" placeholder="没有请留空"/></div>
</div><br/>
<div class="form-group">
<label class="col-sm-3 control-label">代理密码</label>
<div class="col-sm-9"><input type="text" name="proxy_pwd" value="{:config_get('proxy_pwd')}" class="form-control" placeholder="没有请留空"/></div>
</div><br/>
<div class="form-group">
<label class="col-sm-3 control-label">代理协议</label>
<div class="col-sm-9"><select class="form-control" name="proxy_type" default="{:config_get('proxy_type')}">
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
<option value="sock4">SOCK4</option>
<option value="sock5">SOCK5</option>
</select></div>
</div><br/>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9"><input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/><br/>
<a href="javascript:proxytest()" class="btn btn-default btn-block">测试连通性</a></div>
</div>
</form>
</div>
<div class="panel-footer">
为保证代理稳定性建议在本机安装Xray入站代理使用普通http协议仅监听本地IP出站代理使用加密协议连接其他服务器。
</div>
</div>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
<script>
var items = $("select[default]");
for (i = 0; i < items.length; i++) {
$(items[i]).val($(items[i]).attr("default")||0);
}
function saveSetting(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '',
data : $(obj).serialize(),
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert('设置保存成功!', {
icon: 1,
closeBtn: false
}, function(){
window.location.reload()
});
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
return false;
}
function proxytest(){
var proxy_server = $("input[name='proxy_server']").val();
var proxy_port = $("input[name='proxy_port']").val();
var proxy_user = $("input[name='proxy_user']").val();
var proxy_pwd = $("input[name='proxy_pwd']").val();
var proxy_type = $("select[name='proxy_type']").val();
if(proxy_server=='' || proxy_port==''){
layer.alert('代理服务器和端口不能为空!');
return false;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/system/proxytest',
data : {proxy_server:proxy_server, proxy_port:proxy_port, proxy_user:proxy_user, proxy_pwd:proxy_pwd, proxy_type:proxy_type},
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert('连通性测试成功!', {icon: 1})
}else{
layer.alert('连通性测试失败:'+data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
}
</script>
{/block}

View File

@ -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'
];

View File

@ -7,5 +7,7 @@ return [
'commands' => [
'dmtask' => 'app\command\Dmtask',
'opiptask' => 'app\command\Opiptask',
'certtask' => 'app\command\Certtask',
'reset' => 'app\command\Reset',
],
];

View File

@ -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}

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