Compare commits

..

45 Commits
2.6.0 ... 2.7.1

Author SHA1 Message Date
net909
6418c3a2ee 支持批量修改部署任务关联证书 2025-06-05 17:03:00 +08:00
net909
efd18676f3 界面优化 2025-06-04 22:52:38 +08:00
net909
5ba7c324af 优化添加部署账户页面 修复部分已知问题 2025-06-03 22:06:02 +08:00
net909
236610d8fb 证书订单支持设置手动续期 2025-05-31 21:34:43 +08:00
net909
0015015b7a 修复lecdn部署 2025-05-31 19:11:40 +08:00
net909
f776b9f47f 支持图形验证码关闭 2025-05-29 17:30:21 +08:00
消失的彩虹海
0860624bd5 Merge pull request #226 from FantajiNeko/main
修复证书申请代理设置,使用反代时不使用代理
2025-05-29 17:01:05 +08:00
Fantajī × Neko
e328dc6808 新增SOCK5H代理类型,远程解析主机名避免DNS污染 2025-05-29 16:43:51 +08:00
Fantajī × Neko
e58d8f4af1 修复证书申请代理设置,使用反代时不使用代理 2025-05-29 15:10:47 +08:00
net909
9d4260062c 修复ucloud证书 2025-05-25 20:44:04 +08:00
net909
17c50e4ba1 修复二级域名申请SSL 2025-05-25 20:35:23 +08:00
net909
55272fd51b 修复自动获取EAB 2025-05-25 12:29:54 +08:00
net909
a1e4476603 Merge branch 'main' of github.com:netcccyun/dnsmgr 2025-05-25 11:46:05 +08:00
net909
3734e98048 新增宝塔云WAF部署 支持修改域名到期时间 2025-05-25 11:45:20 +08:00
消失的彩虹海
372018c03a Merge pull request #218 from devhaozi/main
ZeroSSL和Google EAB支持自动获取及Google反向代理
2025-05-18 22:31:38 +08:00
耗子
9b037834ad 添加反向代理配置参考 2025-05-18 16:39:04 +08:00
耗子
bf05d51d08 修复缩进问题 2025-05-18 16:28:51 +08:00
耗子
ec89fd685b Google EAB支持自动获取及接口反向代理 2025-05-18 16:16:15 +08:00
耗子
300686aa0a ZeroSSL EAB支持自动获取 2025-05-18 14:25:08 +08:00
net909
8a158ea0a5 Merge branch 'main' of github.com:netcccyun/dnsmgr 2025-05-18 10:06:51 +08:00
消失的彩虹海
8a41c1642a Merge pull request #211 from devhaozi/main
支持1Panel v2部署
2025-05-18 10:03:46 +08:00
耗子
6e3350afbd 支持1Panel v2部署 2025-05-16 15:36:27 +08:00
net909
b5f74368d2 Merge branch 'main' of github.com:netcccyun/dnsmgr 2025-05-16 09:33:24 +08:00
消失的彩虹海
49047db438 Merge pull request #209 from devhaozi/main
支持耗子面板部署
2025-05-16 09:32:57 +08:00
耗子
7c54d8af44 修复行分隔符问题 2025-05-16 00:49:29 +08:00
耗子
9b7a7c2d60 支持耗子面板部署 2025-05-16 00:48:11 +08:00
net909
8e7adead48 Merge branch 'main' of github.com:netcccyun/dnsmgr 2025-05-13 10:44:45 +08:00
消失的彩虹海
1e747a8e9e Merge pull request #204 from HanadaLee/main
支持部署证书到网宿CDN、AWS ACM以及修复AWS CloudFront、阿里云OSS部署
2025-05-13 10:39:53 +08:00
Hanada
a13fb38e66 修复AWS CloudFront部署函数中错误的入参 2025-05-13 02:04:28 +08:00
Hanada
3c6944a701 Merge pull request #5 from HanadaLee/wangsudeploy
支持网宿CDN部署
2025-05-13 02:02:43 +08:00
Hanada
c141089c69 支持网宿CDN部署 2025-05-13 02:00:37 +08:00
Hanada
994bdc7fa3 Merge pull request #4 from HanadaLee/aliyunossdeploy
修复部署证书到阿里云OSS失败
2025-05-12 02:30:19 +08:00
Hanada
842b2aa2d9 修复部署证书到阿里云OSS失败 2025-05-12 02:28:13 +08:00
Hanada
4850250f3c Merge pull request #3 from HanadaLee/awsdeploy
支持直接部署证书到AWS ACM
2025-05-12 00:47:11 +08:00
Hanada
f8add88e3d 支持直接部署证书到AWS ACM 2025-05-12 00:43:33 +08:00
Hanada
dcc440c1f9 修复初次上传证书到ACM的逻辑 2025-05-12 00:02:16 +08:00
Hanada
a4d3cdd612 重构证书上传AWS ACM逻辑 2025-05-11 23:40:35 +08:00
Hanada
a483476e6b Merge pull request #2 from HanadaLee/awsdeploy
修复AWS部署失败
2025-05-11 23:18:32 +08:00
Hanada
be55d4b67d Merge pull request #1 from HanadaLee/wangsudeploy
支持网宿CDNPro部署
2025-05-11 23:17:58 +08:00
Hanada
fa47ffb080 修复AWS部署失败 2025-05-11 23:16:15 +08:00
Hanada
333aacaab9 网宿部署增加产品字段,方便后续扩充其他产品线 2025-05-11 21:12:38 +08:00
Hanada
0752f07f7d 支持网宿CDNPro部署
修复spKey逻辑判断问题

支持网宿CDNPro部署
2025-05-11 20:38:47 +08:00
Hanada
4310ccb770 修复CloudFront部署异常 2025-05-10 21:09:12 +08:00
net909
fb8fe3526b 增加证书API接口 2025-05-06 12:16:02 +08:00
net909
cf36b4bd51 部分SSL部署支持批量 2025-05-05 17:37:53 +08:00
86 changed files with 4336 additions and 3060 deletions

View File

@@ -195,6 +195,10 @@ SSL证书自动部署功能
⭐ 如果您觉得本项目对您有帮助,欢迎给项目点个 Star
🤝 捐赠:
<img height="240" src="https://wkphoto.bj.bcebos.com/d8f9d72a6059252db065f556249b033b5bb5b976.jpg">
### 其他推荐
- [彩虹云主机 - 免备案CDN/虚拟主机](https://www.cccyun.net/)

View File

@@ -326,6 +326,8 @@ function check_proxy($url, $proxy_server, $proxy_port, $type, $proxy_user, $prox
$proxy_type = CURLPROXY_SOCKS4;
} elseif ($type == 'sock5') {
$proxy_type = CURLPROXY_SOCKS5;
} elseif ($type == 'sock5h') {
$proxy_type = CURLPROXY_SOCKS5_HOSTNAME;
} else {
$proxy_type = CURLPROXY_HTTP;
}
@@ -392,7 +394,7 @@ function clearDirectory($dir): bool
return true;
}
function curl_client($url, $data = null, $referer = null, $cookie = null, $headers = null, $proxy = false, $method = null, $timeout = 5)
function curl_client($url, $data = null, $referer = null, $cookie = null, $headers = null, $proxy = false, $method = null, $timeout = 5, $default_headers = true)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
@@ -400,11 +402,15 @@ function curl_client($url, $data = null, $referer = null, $cookie = null, $heade
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$httpheader[] = "Accept: */*";
$httpheader[] = "Accept-Language: zh-CN,zh;q=0.8";
$httpheader[] = "Connection: close";
if ($headers) {
$httpheader = array_merge($httpheader, $headers);
if ($default_headers === true) {
$httpheader[] = "Accept: */*";
$httpheader[] = "Accept-Language: zh-CN,zh;q=0.8";
$httpheader[] = "Connection: close";
if ($headers) {
$httpheader = array_merge($headers, $httpheader);
}
} else {
$httpheader = $headers;
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36");
@@ -430,8 +436,9 @@ function curl_client($url, $data = null, $referer = null, $cookie = null, $heade
$ret = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
@@ -457,6 +464,8 @@ function curl_set_proxy(&$ch)
$proxy_type = CURLPROXY_SOCKS4;
} elseif ($proxy_type == 'sock5') {
$proxy_type = CURLPROXY_SOCKS5;
} elseif ($proxy_type == 'sock5h') {
$proxy_type = CURLPROXY_SOCKS5_HOSTNAME;
} else {
$proxy_type = CURLPROXY_HTTP;
}
@@ -491,7 +500,7 @@ function getDomainDate($domain)
$info = $whois->loadDomainInfo($domain);
if ($info) {
if ($info->expirationDate > 0) {
return [date('Y-m-d H:i:s', $info->creationDate), date('Y-m-d H:i:s', $info->expirationDate)];
return [$info->creationDate > 0 ? date('Y-m-d H:i:s', $info->creationDate) : null, date('Y-m-d H:i:s', $info->expirationDate)];
} else {
throw new Exception('域名到期时间未知');
}

View File

@@ -26,7 +26,7 @@ class Auth extends BaseController
if (empty($username) || empty($password)) {
return json(['code' => -1, 'msg' => '用户名或密码不能为空']);
}
if (!captcha_check($code)) {
if (config_get('vcode', '1') == '1' && !captcha_check($code)) {
return json(['code' => -1, 'msg' => '验证码错误', 'vcode' => 1]);
}
if (file_exists($login_limit_file)) {

View File

@@ -43,8 +43,17 @@ class Cert extends BaseController
$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'];
if ($deploy == 1) {
if (!empty($row['type']) && isset(DeployHelper::$deploy_config[$row['type']])) {
$row['typename'] = DeployHelper::$deploy_config[$row['type']]['name'];
$row['icon'] = DeployHelper::$deploy_config[$row['type']]['icon'];
}
} else {
if (!empty($row['type']) && isset(CertHelper::$cert_config[$row['type']])) {
$row['typename'] = CertHelper::$cert_config[$row['type']]['name'];
$row['icon'] = CertHelper::$cert_config[$row['type']]['icon'];
}
}
$list[] = $row;
}
@@ -66,7 +75,7 @@ class Cert extends BaseController
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.'已存在']);
return json(['code' => -1, 'msg' => $title . '已存在']);
}
Db::startTrans();
$id = Db::name('cert_account')->insertGetId([
@@ -80,15 +89,15 @@ class Cert extends BaseController
try {
$this->checkAccount($id, $type, $deploy);
Db::commit();
return json(['code' => 0, 'msg' => '添加'.$title.'成功!']);
} catch(Exception $e) {
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.'不存在']);
if (!$row) return json(['code' => -1, 'msg' => $title . '不存在']);
$type = input('post.type');
$name = input('post.name', null, 'trim');
$config = input('post.config', null, 'trim');
@@ -96,7 +105,7 @@ class Cert extends BaseController
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.'已存在']);
return json(['code' => -1, 'msg' => $title . '已存在']);
}
Db::startTrans();
Db::name('cert_account')->where('id', $id)->update([
@@ -108,19 +117,19 @@ class Cert extends BaseController
try {
$this->checkAccount($id, $type, $deploy);
Db::commit();
return json(['code' => 0, 'msg' => '修改'.$title.'成功!']);
} catch(Exception $e) {
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){
if ($deploy == 0) {
$dcount = DB::name('cert_order')->where('aid', $id)->count();
if ($dcount > 0) return json(['code' => -1, 'msg' => '该'.$title.'下存在证书订单,无法删除']);
}else{
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.'下存在自动部署任务,无法删除']);
if ($dcount > 0) return json(['code' => -1, 'msg' => '该' . $title . '下存在自动部署任务,无法删除']);
}
Db::name('cert_account')->where('id', $id)->delete();
return json(['code' => 0]);
@@ -139,7 +148,7 @@ class Cert extends BaseController
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.'不存在');
if (empty($account)) return $this->alert('error', $title . '不存在');
}
$typeList = $deploy == 1 ? DeployHelper::getList() : CertHelper::getList();
@@ -156,32 +165,32 @@ class Cert extends BaseController
private function checkAccount($id, $type, $deploy)
{
if($deploy == 0){
if ($deploy == 0) {
$mod = CertHelper::getModel($id);
if($mod){
try{
if ($mod) {
try {
$ext = $mod->register();
if(is_array($ext)){
Db::name('cert_account')->where('id', $id)->update(['ext'=>json_encode($ext)]);
if (is_array($ext)) {
Db::name('cert_account')->where('id', $id)->update(['ext' => json_encode($ext)]);
}
return true;
}catch(Exception $e){
} catch (Exception $e) {
throw new Exception('验证SSL证书账户失败' . $e->getMessage());
}
}else{
throw new Exception('SSL证书申请模块'.$type.'不存在');
} else {
throw new Exception('SSL证书申请模块' . $type . '不存在');
}
}else{
} else {
$mod = DeployHelper::getModel($id);
if($mod){
try{
if ($mod) {
try {
$mod->check();
return true;
}catch(Exception $e){
} catch (Exception $e) {
throw new Exception('验证自动部署账户失败,' . $e->getMessage());
}
}else{
throw new Exception('SSL证书申请模块'.$type.'不存在');
} else {
throw new Exception('SSL证书申请模块' . $type . '不存在');
}
}
}
@@ -190,7 +199,7 @@ class Cert extends BaseController
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$types = [];
foreach(CertHelper::$cert_config as $key=>$value){
foreach (CertHelper::$cert_config as $key => $value) {
$types[$key] = $value['name'];
}
View::assign('types', $types);
@@ -202,18 +211,22 @@ class Cert extends BaseController
if (!checkPermission(2)) return $this->alert('error', '无权限');
$domain = $this->request->post('domain', null, 'trim');
$id = input('post.id');
$aid = input('post.aid', null, 'trim');
$type = input('post.type', null, 'trim');
$status = input('post.status', null, 'trim');
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$select = Db::name('cert_order')->alias('A')->join('cert_account B', 'A.aid = B.id');
$select = Db::name('cert_order')->alias('A')->leftJoin('cert_account B', 'A.aid = B.id');
if (!empty($id)) {
$select->where('A.id', $id);
}elseif (!empty($domain)) {
} elseif (!empty($domain)) {
$oids = Db::name('cert_domain')->where('domain', 'like', '%' . $domain . '%')->column('oid');
$select->whereIn('A.id', $oids);
}
if (!empty($aid)) {
$select->where('A.aid', $aid);
}
if (!empty($type)) {
$select->where('B.type', $type);
}
@@ -233,17 +246,32 @@ class Cert extends BaseController
$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');
if (!empty($row['type']) && isset(CertHelper::$cert_config[$row['type']])) {
$row['typename'] = CertHelper::$cert_config[$row['type']]['name'];
$row['icon'] = CertHelper::$cert_config[$row['type']]['icon'];
} else {
$row['typename'] = null;
}
$row['domains'] = Db::name('cert_domain')->where('oid', $row['id'])->order('sort', 'ASC')->column('domain');
$row['end_day'] = $row['expiretime'] ? ceil((strtotime($row['expiretime']) - time()) / 86400) : null;
if($row['error']) $row['error'] = htmlspecialchars(str_replace("'", "\\'", $row['error']));
if ($row['error']) $row['error'] = htmlspecialchars(str_replace("'", "\\'", $row['error']));
$list[] = $row;
}
return json(['total' => $total, 'rows' => $list]);
}
public function order_info()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$id = input('post.id/d');
$row = Db::name('cert_order')->where('id', $id)->find();
if (!$row) return json(['code' => -1, 'msg' => '证书订单不存在']);
$pfx = CertHelper::getPfx($row['fullchain'], $row['privatekey']);
$row['pfx'] = base64_encode($pfx);
return json(['code' => 0, 'data' => ['id' => $row['id'], 'crt' => $row['fullchain'], 'key' => $row['privatekey'], 'pfx' => $row['pfx'], 'issuetime' => $row['issuetime'], 'expiretime' => $row['expiretime'], 'domains' => Db::name('cert_domain')->where('oid', $row['id'])->order('sort', 'ASC')->column('domain')]]);
}
public function order_op()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
@@ -257,31 +285,66 @@ class Cert extends BaseController
$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'),
'issuer' => '',
'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' => '必填参数不能为空']);
$aid = input('post.aid/d');
$res = $this->check_order($order, $domains);
if (is_array($res)) return json($res);
if ($aid == -1) {
$fullchain = input('post.fullchain', null, 'trim');
$privatekey = input('post.privatekey', null, 'trim');
$certInfo = $this->parse_cert_key($fullchain, $privatekey);
if ($certInfo['code'] == -1) return json($certInfo);
$domains = $certInfo['domains'];
$order_ids = Db::name('cert_order')->where('issuetime', $certInfo['issuetime'])->column('id');
if (!empty($order_ids)) {
foreach ($order_ids as $order_id) {
$domains2 = Db::name('cert_domain')->where('oid', $order_id)->column('domain');
if (arrays_are_equal($domains2, $domains)) {
return json(['code' => -1, 'msg' => '该证书已存在,无需重复添加']);
}
}
}
$order = [
'aid' => 0,
'keytype' => $certInfo['keytype'],
'keysize' => $certInfo['keysize'],
'addtime' => date('Y-m-d H:i:s'),
'updatetime' => date('Y-m-d H:i:s'),
'issuetime' => $certInfo['issuetime'],
'expiretime' => $certInfo['expiretime'],
'issuer' => $certInfo['issuer'],
'status' => 3,
'isauto' => 1,
'fullchain' => $fullchain,
'privatekey' => $privatekey,
];
} else {
$order = [
'aid' => $aid,
'keytype' => input('post.keytype'),
'keysize' => input('post.keysize'),
'addtime' => date('Y-m-d H:i:s'),
'issuer' => '',
'status' => 0,
'isauto' => 1,
];
$domains = input('post.domains', [], 'trim');
$domains = array_map('trim', $domains);
$domains = array_filter($domains, function ($v) {
return !empty($v);
});
$domains = array_unique($domains);
if (empty($domains)) return json(['code' => -1, 'msg' => '绑定域名不能为空']);
$res = $this->check_order($order, $domains);
if (is_array($res)) return json($res);
}
if (empty($order['keytype']) || empty($order['keysize'])) return json(['code' => -1, 'msg' => '必填参数不能为空']);
Db::startTrans();
$id = Db::name('cert_order')->insertGetId($order);
$domainList = [];
$i=1;
foreach($domains as $domain){
$i = 1;
foreach ($domains as $domain) {
$domainList[] = [
'oid' => $id,
'domain' => convertDomainToAscii($domain),
@@ -295,31 +358,53 @@ class Cert extends BaseController
$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);
$aid = input('post.aid/d');
if ($aid == -1) {
$fullchain = input('post.fullchain', null, 'trim');
$privatekey = input('post.privatekey', null, 'trim');
$certInfo = $this->parse_cert_key($fullchain, $privatekey);
if ($certInfo['code'] == -1) return json($certInfo);
$domains = $certInfo['domains'];
$order = [
'aid' => 0,
'keytype' => $certInfo['keytype'],
'keysize' => $certInfo['keysize'],
'updatetime' => date('Y-m-d H:i:s'),
'issuetime' => $certInfo['issuetime'],
'expiretime' => $certInfo['expiretime'],
'issuer' => $certInfo['issuer'],
'status' => 3,
'issend' => 0,
'fullchain' => $fullchain,
'privatekey' => $privatekey,
];
} else {
$domains = input('post.domains', [], 'trim');
$order = [
'aid' => $aid,
'keytype' => input('post.keytype'),
'keysize' => input('post.keysize'),
'updatetime' => date('Y-m-d H:i:s'),
];
$domains = array_map('trim', $domains);
$domains = array_filter($domains, function ($v) {
return !empty($v);
});
$domains = array_unique($domains);
if (empty($domains)) return json(['code' => -1, 'msg' => '绑定域名不能为空']);
$res = $this->check_order($order, $domains);
if (is_array($res)) return json($res);
}
if (empty($order['keytype']) || empty($order['keysize'])) return json(['code' => -1, 'msg' => '必填参数不能为空']);
Db::startTrans();
Db::name('cert_order')->where('id', $id)->update($order);
Db::name('cert_domain')->where('oid', $id)->delete();
$domainList = [];
$i=1;
foreach($domains as $domain){
$i = 1;
foreach ($domains as $domain) {
$domainList[] = [
'oid' => $id,
'domain' => convertDomainToAscii($domain),
@@ -329,79 +414,13 @@ class Cert extends BaseController
Db::name('cert_domain')->insertAll($domainList);
Db::commit();
return json(['code' => 0, 'msg' => '修改证书订单成功!']);
} elseif ($action == 'import') {
$fullchain = input('post.fullchain', null, 'trim');
$privatekey = input('post.privatekey', null, 'trim');
if (!openssl_x509_read($fullchain)) return json(['code' => -1, 'msg' => '证书内容填写错误']);
if (!openssl_get_privatekey($privatekey)) return json(['code' => -1, 'msg' => '私钥内容填写错误']);
if (!openssl_x509_check_private_key($fullchain, $privatekey)) return json(['code' => -1, 'msg' => 'SSL证书与私钥不匹配']);
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo || !isset($certInfo['extensions']['subjectAltName'])) return json(['code' => -1, 'msg' => '证书内容解析失败']);
$domains = [];
$subjectAltName = explode(',', $certInfo['extensions']['subjectAltName']);
foreach ($subjectAltName as $domain) {
$domain = trim($domain);
if (strpos($domain, 'DNS:') === 0) $domain = substr($domain, 4);
if (!empty($domain)) {
$domains[] = $domain;
}
}
$domains = array_unique($domains);
if (empty($domains)) return json(['code' => -1, 'msg' => '证书绑定域名不能为空']);
$issuetime = date('Y-m-d H:i:s', $certInfo['validFrom_time_t']);
$expiretime = date('Y-m-d H:i:s', $certInfo['validTo_time_t']);
$issuer = $certInfo['issuer']['CN'];
$order_ids = Db::name('cert_order')->where('issuetime', $issuetime)->column('id');
if (!empty($order_ids)) {
foreach ($order_ids as $order_id) {
$domains2 = Db::name('cert_domain')->where('oid', $order_id)->column('domain');
if (arrays_are_equal($domains2, $domains)) {
return json(['code' => -1, 'msg' => '该证书已存在,无需重复添加']);
}
}
}
$order = [
'aid' => input('post.aid/d'),
'keytype' => input('post.keytype'),
'keysize' => input('post.keysize'),
'addtime' => date('Y-m-d H:i:s'),
'updatetime' => date('Y-m-d H:i:s'),
'issuetime' => $issuetime,
'expiretime' => $expiretime,
'issuer' => $issuer,
'status' => 3,
'fullchain' => $fullchain,
'privatekey' => $privatekey,
];
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 == 'del') {
$id = input('post.id/d');
$dcount = DB::name('cert_deploy')->where('oid', $id)->count();
if ($dcount > 0) return json(['code' => -1, 'msg' => '该证书关联了自动部署任务,无法删除']);
try{
try {
(new CertOrderService($id))->cancel();
}catch(Exception $e){
} catch (Exception $e) {
}
Db::name('cert_order')->where('id', $id)->delete();
Db::name('cert_domain')->where('oid', $id)->delete();
@@ -413,28 +432,28 @@ class Cert extends BaseController
return json(['code' => 0]);
} elseif ($action == 'reset') {
$id = input('post.id/d');
try{
try {
$service = new CertOrderService($id);
$service->cancel();
$service->reset();
return json(['code' => 0]);
}catch(Exception $e){
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
} elseif ($action == 'revoke') {
$id = input('post.id/d');
try{
try {
$service = new CertOrderService($id);
$service->revoke();
return json(['code' => 0]);
}catch(Exception $e){
} 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)]);
$file = app()->getRuntimePath() . 'log/' . $processid . '.log';
if (!file_exists($file)) return json(['code' => -1, 'msg' => '日志文件不存在']);
return json(['code' => 0, 'data' => file_get_contents($file), 'time' => filemtime($file)]);
} elseif ($action == 'operation') {
$ids = input('post.ids');
$success = 0;
@@ -477,24 +496,79 @@ class Cert extends BaseController
$cname = CertHelper::$cert_config[$account['type']]['cname'];
if (count($domains) > $max_domains) {
if (!(count($domains) == 2 && $max_domains == 1 && ltrim($domains[0], 'www.') == ltrim($domains[1], 'www.'))) {
return ['code' => -1, 'msg' => '域名数量不能超过'.$max_domains.'个'];
return ['code' => -1, 'msg' => '域名数量不能超过' . $max_domains . '个'];
}
}
foreach($domains as $domain){
foreach ($domains as $domain) {
if (!$wildcard && strpos($domain, '*') !== false) return ['code' => -1, 'msg' => '该证书账户类型不支持泛域名'];
$mainDomain = getMainDomain($domain);
$drow = Db::name('domain')->where('name', $mainDomain)->find();
if (!$drow) {
if (substr($domain, 0, 2) == '*.') $domain = substr($domain, 2);
if (!$cname || !Db::name('cert_cname')->where('domain', $domain)->where('status', 1)->find()) {
return ['code' => -1, 'msg' => '域名'.$domain.'未在本系统添加'];
return ['code' => -1, 'msg' => '域名' . $domain . '未在本系统添加'];
}
}
}
return true;
}
private function parse_cert_key($fullchain, $privatekey)
{
if (!openssl_x509_read($fullchain)) return ['code' => -1, 'msg' => '证书内容填写错误'];
if (!openssl_get_privatekey($privatekey)) return ['code' => -1, 'msg' => '私钥内容填写错误'];
if (!openssl_x509_check_private_key($fullchain, $privatekey)) return ['code' => -1, 'msg' => 'SSL证书与私钥不匹配'];
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo || !isset($certInfo['extensions']['subjectAltName'])) return ['code' => -1, 'msg' => '证书内容解析失败'];
$pubKey = openssl_pkey_get_public($fullchain);
if (!$pubKey) return ['code' => -1, 'msg' => '证书公钥解析失败'];
$keyDetails = openssl_pkey_get_details($pubKey);
$keytype = null;
$keysize = 0;
switch ($keyDetails['type']) {
case OPENSSL_KEYTYPE_RSA:
$keytype = 'RSA';
$keysize = $keyDetails['bits'];
break;
case OPENSSL_KEYTYPE_EC:
$keytype = 'ECC';
$keysize = $keyDetails['bits'];
break;
case OPENSSL_KEYTYPE_DSA:
$keytype = 'DSA';
$keysize = $keyDetails['bits'];
break;
default:
$keytype = 'Unknown';
}
$domains = [];
$subjectAltName = explode(',', $certInfo['extensions']['subjectAltName']);
foreach ($subjectAltName as $domain) {
$domain = trim($domain);
if (strpos($domain, 'DNS:') === 0) $domain = substr($domain, 4);
if (!empty($domain)) {
$domains[] = $domain;
}
}
$domains = array_unique($domains);
if (empty($domains)) return ['code' => -1, 'msg' => '证书绑定域名不能为空'];
$issuetime = date('Y-m-d H:i:s', $certInfo['validFrom_time_t']);
$expiretime = date('Y-m-d H:i:s', $certInfo['validTo_time_t']);
$issuer = $certInfo['issuer']['CN'];
return [
'code' => 0,
'keytype' => $keytype,
'keysize' => $keysize,
'issuetime' => $issuetime,
'expiretime' => $expiretime,
'issuer' => $issuer,
'domains' => $domains,
];
}
public function order_process()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
@@ -506,18 +580,18 @@ class Cert extends BaseController
}
$id = input('post.id/d');
$reset = input('post.reset/d', 0);
try{
try {
$service = new CertOrderService($id);
if($reset == 1){
if ($reset == 1) {
$service->reset();
}
$retcode = $service->process(true);
if($retcode == 3){
if ($retcode == 3) {
return json(['code' => 0, 'msg' => '证书已签发成功!']);
}elseif($retcode == 1){
} elseif ($retcode == 1) {
return json(['code' => 0, 'msg' => '添加DNS记录成功请等待DNS生效后点击验证']);
}
}catch(Exception $e){
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage(), 'trace' => $e->getTrace()]);
}
}
@@ -530,14 +604,16 @@ class Cert extends BaseController
$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();
$order = Db::name('cert_order')->where('id', $id)->fieldRaw('id,aid,keytype,keysize,status,fullchain,privatekey')->find();
if (empty($order)) return $this->alert('error', '证书订单不存在');
$order['domains'] = Db::name('cert_domain')->where('oid', $order['id'])->order('sort','ASC')->column('domain');
$order['domains'] = Db::name('cert_domain')->where('oid', $order['id'])->order('sort', 'ASC')->column('domain');
if ($order['aid'] == 0) $order['aid'] = -1;
}
$accounts = [];
foreach (Db::name('cert_account')->where('deploy', 0)->select() as $row) {
$accounts[$row['id']] = ['name'=>$row['id'].'_'.CertHelper::$cert_config[$row['type']]['name'], 'type'=>$row['type']];
if (empty($row['type']) || !isset(CertHelper::$cert_config[$row['type']])) continue;
$accounts[$row['id']] = ['name' => $row['id'] . '_' . CertHelper::$cert_config[$row['type']]['name'], 'type' => $row['type']];
if (!empty($row['remark'])) {
$accounts[$row['id']]['name'] .= '' . $row['remark'] . '';
}
@@ -549,26 +625,11 @@ class Cert extends BaseController
return View::fetch();
}
public function order_import()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$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);
return View::fetch();
}
public function deploytask()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$types = [];
foreach(DeployHelper::$deploy_config as $key=>$value){
foreach (DeployHelper::$deploy_config as $key => $value) {
$types[$key] = $value['name'];
}
View::assign('types', $types);
@@ -580,19 +641,23 @@ class Cert extends BaseController
if (!checkPermission(2)) return $this->alert('error', '无权限');
$domain = $this->request->post('domain', null, 'trim');
$oid = input('post.oid');
$aid = input('post.aid', null, 'trim');
$type = input('post.type', null, 'trim');
$status = input('post.status', null, 'trim');
$remark = input('post.remark', null, 'trim');
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$select = Db::name('cert_deploy')->alias('A')->join('cert_account B', 'A.aid = B.id')->join('cert_order C', 'A.oid = C.id')->join('cert_account D', 'C.aid = D.id');
$select = Db::name('cert_deploy')->alias('A')->leftJoin('cert_account B', 'A.aid = B.id')->leftJoin('cert_order C', 'A.oid = C.id')->leftJoin('cert_account D', 'C.aid = D.id');
if (!empty($oid)) {
$select->where('A.oid', $oid);
} elseif (!empty($domain)) {
$oids = Db::name('cert_domain')->where('domain', 'like', '%' . $domain . '%')->column('oid');
$select->whereIn('oid', $oids);
}
if (!empty($aid)) {
$select->where('A.aid', $aid);
}
if (!empty($type)) {
$select->where('B.type', $type);
}
@@ -607,11 +672,17 @@ class Cert extends BaseController
$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']));
if (!empty($row['type']) && isset(DeployHelper::$deploy_config[$row['type']])) {
$row['typename'] = DeployHelper::$deploy_config[$row['type']]['name'];
$row['icon'] = DeployHelper::$deploy_config[$row['type']]['icon'];
}
if (!empty($row['certtype']) && isset(CertHelper::$cert_config[$row['certtype']])) {
$row['certtypename'] = CertHelper::$cert_config[$row['certtype']]['name'];
} else {
$row['certtypename'] = '手动续期';
}
$row['domains'] = Db::name('cert_domain')->where('oid', $row['oid'])->order('sort', 'ASC')->column('domain');
if ($row['error']) $row['error'] = htmlspecialchars(str_replace("'", "\\'", $row['error']));
$list[] = $row;
}
@@ -640,7 +711,7 @@ class Cert extends BaseController
$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'),
@@ -661,21 +732,27 @@ class Cert extends BaseController
return json(['code' => 0]);
} elseif ($action == 'reset') {
$id = input('post.id/d');
try{
try {
$service = new CertDeployService($id);
$service->reset();
return json(['code' => 0]);
}catch(Exception $e){
} 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)]);
$file = app()->getRuntimePath() . 'log/' . $processid . '.log';
if (!file_exists($file)) return json(['code' => -1, 'msg' => '日志文件不存在']);
return json(['code' => 0, 'data' => file_get_contents($file), 'time' => filemtime($file)]);
} elseif ($action == 'operation') {
$ids = input('post.ids');
$success = 0;
$certid = 0;
if (input('post.action') == 'cert') {
$certid = input('post.certid/d');
$cert = Db::name('cert_order')->where('id', $certid)->find();
if (!$cert) return json(['code' => -1, 'msg' => '证书订单不存在']);
}
foreach ($ids as $id) {
if (input('post.action') == 'delete') {
Db::name('cert_deploy')->where('id', $id)->delete();
@@ -691,6 +768,9 @@ class Cert extends BaseController
$active = input('post.action') == 'open' ? 1 : 0;
Db::name('cert_deploy')->where('id', $id)->update(['active' => $active]);
$success++;
} elseif (input('post.action') == 'cert') {
Db::name('cert_deploy')->where('id', $id)->update(['oid' => $certid]);
$success++;
}
}
return json(['code' => 0, 'msg' => '成功操作' . $success . '个任务']);
@@ -709,14 +789,14 @@ class Cert extends BaseController
}
$id = input('post.id/d');
$reset = input('post.reset/d', 0);
try{
try {
$service = new CertDeployService($id);
if($reset == 1){
if ($reset == 1) {
$service->reset();
}
$service->process(true);
return json(['code' => 0, 'msg' => 'SSL证书部署任务执行成功']);
}catch(Exception $e){
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage(), 'trace' => $e->getTrace()]);
}
}
@@ -735,7 +815,8 @@ class Cert extends BaseController
$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['type']) || !isset(DeployHelper::$deploy_config[$row['type']])) continue;
$accounts[$row['id']] = ['name' => $row['id'] . '_' . DeployHelper::$deploy_config[$row['type']]['name'], 'type' => $row['type']];
if (!empty($row['remark'])) {
$accounts[$row['id']]['name'] .= '' . $row['remark'] . '';
}
@@ -743,10 +824,15 @@ class Cert extends BaseController
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'].''];
foreach (Db::name('cert_order')->alias('A')->leftJoin('cert_account B', 'A.aid = B.id')->where('status', '<>', 4)->fieldRaw('A.id,A.aid,B.type,B.remark aremark')->order('id', 'desc')->select() as $row) {
$domains = Db::name('cert_domain')->where('oid', $row['id'])->order('sort', 'ASC')->column('domain');
$domainstr = count($domains) > 2 ? implode('、', array_slice($domains, 0, 2)) . '等' . count($domains) . '个域名' : implode('、', $domains);
if ($row['aid'] == 0) {
$name = $row['id'] . '_' . $domainstr . '(手动续期)';
} else {
$name = $row['id'] . '_' . $domainstr . '' . CertHelper::$cert_config[$row['type']]['name'] . '';
}
$orders[$row['id']] = ['name' => $name];
}
View::assign('orders', $orders);
@@ -774,7 +860,7 @@ class Cert extends BaseController
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$select = Db::name('cert_cname')->alias('A')->join('domain B', 'A.did = B.id');
$select = Db::name('cert_cname')->alias('A')->leftJoin('domain B', 'A.did = B.id');
if (!empty($kw)) {
$select->whereLike('A.domain', '%' . $kw . '%');
}
@@ -817,7 +903,7 @@ class Cert extends BaseController
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'].'已存在']);
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记录值']);
@@ -828,7 +914,7 @@ class Cert extends BaseController
$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'),
@@ -855,13 +941,13 @@ class Cert extends BaseController
$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)){
if (!$result || !in_array($record, $result)) {
$result = \app\utils\DnsQueryUtils::query_dns_doh($domain, 'CNAME');
if(!$result || !in_array($record, $result)){
if (!$result || !in_array($record, $result)) {
$status = 0;
}
}
if($status != $row['status']){
if ($status != $row['status']) {
Db::name('cert_cname')->where('id', $id)->update(['status' => $status]);
}
return json(['code' => 0, 'status' => $status]);
@@ -871,17 +957,6 @@ class Cert extends BaseController
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

@@ -222,20 +222,6 @@ class Dmonitor extends BaseController
return json(['total' => $total, 'rows' => $list]);
}
public function noticeset()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$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']);
}
public function clean()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');

View File

@@ -250,12 +250,14 @@ class Domain extends BaseController
$is_hide = input('post.is_hide/d');
$is_sso = input('post.is_sso/d');
$is_notice = input('post.is_notice/d');
$expiretime = input('post.expiretime', null, 'trim');
$remark = input('post.remark', null, 'trim');
if (empty($remark)) $remark = null;
Db::name('domain')->where('id', $id)->update([
'is_hide' => $is_hide,
'is_sso' => $is_sso,
'is_notice' => $is_notice,
'expiretime' => $expiretime ? $expiretime : null,
'remark' => $remark,
]);
return json(['code' => 0, 'msg' => '修改域名配置成功!']);
@@ -419,8 +421,8 @@ class Domain extends BaseController
$type = input('post.type', null, 'trim');
$line = input('post.line', null, 'trim');
$status = input('post.status', null, 'trim');
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$offset = input('post.offset/d', 0);
$limit = input('post.limit/d', 10);
if ($limit == 0) {
$page = 1;
} else {
@@ -543,10 +545,14 @@ class Domain extends BaseController
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);
$recordid = $dns->updateDomainRecord($recordid, $name, $type, $value, $line, $ttl, $mx, $weight, $remark);
if ($recordid) {
if (is_array($recordinfo['Value'])) $recordinfo['Value'] = implode(',', $recordinfo['Value']);
if ($recordinfo['Name'] != $name || $recordinfo['Type'] != $type || $recordinfo['Value'] != $value) {
$this->add_log($drow['name'], '修改解析', $recordinfo['Name'].' ['.$recordinfo['Type'].'] '.$recordinfo['Value'].' → '.$name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')');
} elseif($recordinfo['Line'] != $line || $recordinfo['TTL'] != $ttl) {
if ($recordinfo) {
if (is_array($recordinfo['Value'])) $recordinfo['Value'] = implode(',', $recordinfo['Value']);
if ($recordinfo['Name'] != $name || $recordinfo['Type'] != $type || $recordinfo['Value'] != $value) {
$this->add_log($drow['name'], '修改解析', $recordinfo['Name'].' ['.$recordinfo['Type'].'] '.$recordinfo['Value'].' → '.$name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')');
} elseif($recordinfo['Line'] != $line || $recordinfo['TTL'] != $ttl) {
$this->add_log($drow['name'], '修改解析', $name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')');
}
} else {
$this->add_log($drow['name'], '修改解析', $name.' ['.$type.'] '.$value.' (线路:'.$line.' TTL:'.$ttl.')');
}
return json(['code' => 0, 'msg' => '修改解析记录成功!']);
@@ -790,6 +796,9 @@ class Domain extends BaseController
}
if (is_null($line)) {
$line = DnsHelper::$line_name[$dnstype]['DEF'];
if ($dnstype == 'cloudflare' && input('post.proxy/d', 0) == 1) {
$line = '1';
}
}
$dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']);

View File

@@ -10,41 +10,33 @@ use think\facade\Cache;
class System extends BaseController
{
public function set()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$params = input('post.');
if (isset($params['mail_type']) && isset($params['mail_name2']) && $params['mail_type'] > 0) {
$params['mail_name'] = $params['mail_name2'];
unset($params['mail_name2']);
}
foreach ($params as $key => $value) {
if (empty($key)) {
continue;
}
config_set($key, $value);
}
Cache::delete('configs');
return json(['code' => 0, 'msg' => 'succ']);
}
public function 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();
}

View File

@@ -288,6 +288,7 @@ uk.com
us.com
uy.com
za.com
it.com
co.cr
ed.cr
fi.cr
@@ -1341,6 +1342,7 @@ zagan.pl
zarow.pl
zgora.pl
zgorzelec.pl
co.pl
co.pn
net.pn
org.pn
@@ -1925,4 +1927,18 @@ edu.kg
edu.cn
eu.org
us.kg
ggff.net
xx.kg
qzz.io
dpdns.org
ggff.net
ac.ru
edu.ru
com.ru
msk.ru
net.ru
nov.ru
org.ru
pp.ru
spb.ru
uk.co
gov.scot

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@ class ACMECert extends ACMEv2
$protected = array(
'alg' => 'HS256',
'kid' => $eab_kid,
'url' => $this->resources['newAccount']
'url' => $this->unproxiedURL($this->resources['newAccount'])
);
$payload = $this->jwk_header['jwk'];

View File

@@ -8,13 +8,22 @@ 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;
$ch = null, $logger = true, $bits, $sha_bits, $directory, $resources, $jwk_header, $kid_header, $account_key, $thumbprint, $nonce = null, $proxy, $proxy_config = null;
private $delay_until = null;
public function __construct($directory, $proxy = false)
{
/**
* @param $directory string ACME directory URL
* @param $proxy int 代理模式0为不使用代理1为使用系统代理2为使用反向代理
* @param null $proxy_config array 反向代理配置proxy参数为2时必填
* @throws Exception
*/
public function __construct($directory, $proxy = 0, $proxy_config = null)
{
$this->directory = $directory;
$this->proxy = $proxy;
if ($proxy == 2) {
$this->proxy_config = $proxy_config;
}
}
public function __destruct()
@@ -190,7 +199,8 @@ class ACMEv2
}
if (!$this->kid_header['kid'] && $type === 'newAccount') {
$this->kid_header['kid'] = $ret['headers']['location'];
// 反向替换反向代理配置,防止破坏签名
$this->kid_header['kid'] = $this->unproxiedURL($ret['headers']['location']);
$this->log('AccountID: ' . $this->kid_header['kid']);
}
@@ -218,7 +228,8 @@ class ACMEv2
throw new Exception('Resource "' . $type . '" not available.');
}
$protected['url'] = $this->resources[$type];
// 反向替换反向代理配置,防止破坏签名
$protected['url'] = $this->unproxiedURL($this->resources[$type]);
$protected64 = $this->base64url(json_encode($protected, JSON_UNESCAPED_SLASHES));
$payload64 = $this->base64url(is_string($payload) ? $payload : json_encode($payload, JSON_UNESCAPED_SLASHES));
@@ -285,6 +296,9 @@ class ACMEv2
$this->delay_until = null;
}
// 替换反向代理配置
$url = $this->proxiedURL($url);
$method = $data === false ? 'HEAD' : ($data === null ? 'GET' : 'POST');
$user_agent = 'ACMECert v3.4.0 (+https://github.com/skoerfgen/ACMECert)';
$header = ($data === null || $data === false) ? array() : array('Content-Type: application/jose+json');
@@ -307,7 +321,7 @@ class ACMEv2
}
));
if ($this->proxy) {
if ($this->proxy == 1) {
curl_set_proxy($this->ch);
}
@@ -406,4 +420,30 @@ class ACMEv2
}, isset($error['subproblems']) ? $error['subproblems'] : array())
);
}
// 替换反向代理配置
protected function proxiedURL($url)
{
if ($this->proxy == 2) {
return str_replace(
$this->proxy_config['origin'],
$this->proxy_config['proxy'],
$url
);
}
return $url;
}
// 反向替换反向代理配置
protected function unproxiedURL($url)
{
if ($this->proxy == 2) {
return str_replace(
$this->proxy_config['proxy'],
$this->proxy_config['origin'],
$url
);
}
return $url;
}
}

View File

@@ -69,7 +69,7 @@ class aliyun implements CertInterface
$dnsList = [];
if ($data['Type'] == 'domain_verify') {
$mainDomain = getMainDomain($domain);
$name = str_replace('.' . $mainDomain, '', $data['RecordDomain']);
$name = substr($data['RecordDomain'], 0, -(strlen($mainDomain) + 1));
$dnsList[$mainDomain][] = ['name' => $name, 'type' => $data['RecordType'], 'value' => $data['RecordValue']];
}

View File

@@ -15,7 +15,7 @@ class customacme implements CertInterface
public function __construct($config, $ext = null)
{
$this->config = $config;
$this->ac = new ACMECert($config['directory'], $config['proxy'] == 1);
$this->ac = new ACMECert($config['directory'], (int)$config['proxy']);
if ($ext) {
$this->ext = $ext;
$this->ac->loadAccountKey($ext['key']);
@@ -64,7 +64,7 @@ class customacme implements CertInterface
if (!empty($order['challenges'])) {
foreach ($order['challenges'] as $opts) {
$mainDomain = getMainDomain($opts['domain']);
$name = str_replace('.' . $mainDomain, '', $opts['key']);
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
}
}

View File

@@ -1,118 +1,142 @@
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\acme\ACMECert;
use Exception;
class google implements CertInterface
{
private $directories = array(
'live' => 'https://dv.acme-v02.api.pki.goog/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);
}
}
<?php
namespace app\lib\cert;
use app\lib\CertInterface;
use app\lib\acme\ACMECert;
use Exception;
class google implements CertInterface
{
private $directories = array(
'live' => 'https://dv.acme-v02.api.pki.goog',
'staging' => 'https://dv.acme-v02.test-api.pki.goog'
);
private $ac;
private $config;
private $ext;
public function __construct($config, $ext = null)
{
$this->config = $config;
if (empty($config['mode'])) $config['mode'] = 'live';
if (empty($config['proxy_url'])) $config['proxy_url'] = '';
$this->ac = new ACMECert($this->directories[$config['mode']] . '/directory', (int)$config['proxy'], [
'origin' => $this->directories[$config['mode']],
'proxy' => rtrim($config['proxy_url'], '/'),
]);
if ($ext) {
$this->ext = $ext;
$this->ac->loadAccountKey($ext['key']);
$this->ac->setAccount($ext['kid']);
}
}
public function register()
{
if (empty($this->config['email'])) throw new Exception('邮件地址不能为空');
if (isset($this->config['eabMode']) && $this->config['eabMode'] == 'auto') {
$eab = $this->getEAB();
} else {
$eab = ['kid' => $this->config['kid'], 'key' => $this->config['key']];
}
if (!empty($this->ext['key'])) {
$kid = $this->ac->registerEAB(true, $eab['kid'], $eab['key'], $this->config['email']);
return ['kid' => $kid, 'key' => $this->ext['key']];
}
$key = $this->ac->generateRSAKey(2048);
$this->ac->loadAccountKey($key);
$kid = $this->ac->registerEAB(true, $eab['kid'], $eab['key'], $this->config['email']);
return ['kid' => $kid, 'key' => $key];
}
public function buyCert($domainList, &$order)
{
}
public function createOrder($domainList, &$order, $keytype, $keysize)
{
$domain_config = [];
foreach ($domainList as $domain) {
if (empty($domain)) continue;
$domain_config[$domain] = ['challenge' => 'dns-01'];
}
if (empty($domain_config)) throw new Exception('域名列表不能为空');
$order = $this->ac->createOrder($domain_config);
$dnsList = [];
if (!empty($order['challenges'])) {
foreach ($order['challenges'] as $opts) {
$mainDomain = getMainDomain($opts['domain']);
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
/*if (!array_key_exists($mainDomain, $dnsList)) {
$dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "pki.goog"'];
}*/
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
}
}
return $dnsList;
}
public function authOrder($domainList, $order)
{
$this->ac->authOrder($order);
}
public function getAuthStatus($domainList, $order)
{
return true;
}
public function finalizeOrder($domainList, $order, $keytype, $keysize)
{
if (empty($domainList)) throw new Exception('域名列表不能为空');
if ($keytype == 'ECC') {
if (empty($keysize)) $keysize = '384';
$private_key = $this->ac->generateECKey($keysize);
} else {
if (empty($keysize)) $keysize = '2048';
$private_key = $this->ac->generateRSAKey($keysize);
}
$fullchain = $this->ac->finalizeOrder($domainList, $order, $private_key);
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $certInfo['issuer']['CN'], 'subject' => $certInfo['subject']['CN'], 'validFrom' => $certInfo['validFrom_time_t'], 'validTo' => $certInfo['validTo_time_t']];
}
public function revoke($order, $pem)
{
$this->ac->revoke($pem);
}
public function cancel($order)
{
}
public function setLogger($func)
{
$this->ac->setLogger($func);
}
private function getEAB()
{
$api = "https://gts.rat.dev/eab";
$response = curl_client($api, null, null, null, null, $this->config['proxy'] == 1, 'GET', 10);
$result = json_decode($response['body'], true);
if (!isset($result['msg'])) {
throw new Exception('解析返回数据失败:' . $response['body']);
} elseif ($result['msg'] != 'success') {
throw new Exception('获取EAB失败' . $result['msg']);
} elseif (empty($result['data']['key_id']) || empty($result['data']['mac_key'])) {
throw new Exception('获取EAB失败返回数据不完整');
}
return ['kid' => $result['data']['key_id'], 'key' => $result['data']['mac_key']];
}
}

View File

@@ -66,7 +66,7 @@ class huoshan implements CertInterface
$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']);
$name = substr($opts['validation_domain'], 0, -(strlen($mainDomain) + 1));
$dnsList[$mainDomain][] = ['name' => $name, 'type' => $type, 'value' => $opts['value']];
}
}

View File

@@ -20,7 +20,7 @@ class letsencrypt implements CertInterface
{
$this->config = $config;
if (empty($config['mode'])) $config['mode'] = 'live';
$this->ac = new ACMECert($this->directories[$config['mode']], $config['proxy'] == 1);
$this->ac = new ACMECert($this->directories[$config['mode']], (int)$config['proxy']);
if ($ext) {
$this->ext = $ext;
$this->ac->loadAccountKey($ext['key']);
@@ -60,7 +60,7 @@ class letsencrypt implements CertInterface
if (!empty($order['challenges'])) {
foreach ($order['challenges'] as $opts) {
$mainDomain = getMainDomain($opts['domain']);
$name = str_replace('.' . $mainDomain, '', $opts['key']);
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
/*if (!array_key_exists($mainDomain, $dnsList)) {
$dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "letsencrypt.org"'];
}*/

View File

@@ -15,14 +15,15 @@ class tencent implements CertInterface
private $service = "ssl";
private $version = "2019-12-05";
private $logger;
private $proxy;
private TencentCloud $client;
public function __construct($config, $ext = null)
{
$this->SecretId = $config['SecretId'];
$this->SecretKey = $config['SecretKey'];
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new TencentCloud($this->SecretId, $this->SecretKey, $this->endpoint, $this->service, $this->version, null, $proxy);
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->client = new TencentCloud($this->SecretId, $this->SecretKey, $this->endpoint, $this->service, $this->version, null, $this->proxy);
$this->email = $config['email'];
}
@@ -59,8 +60,9 @@ class tencent implements CertInterface
$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']];
$mainDomain = getMainDomain($opts['DvAuthKey']);
$name = substr($opts['DvAuthKey'], 0, -(strlen($mainDomain) + 1));
$dnsList[$mainDomain][] = ['name' => $name, 'type' => $opts['DvAuthVerifyType'] ?? 'CNAME', 'value' => $opts['DvAuthValue']];
}
}
@@ -96,13 +98,21 @@ class tencent implements CertInterface
public function finalizeOrder($domainList, $order, $keytype, $keysize)
{
$param = [
'CertificateIds' => [$order['CertificateId']],
'SwitchStatus' => 1,
];
$this->request('ModifyCertificatesExpiringNotificationSwitch', $param);
if (!is_dir(app()->getRuntimePath() . 'cert')) mkdir(app()->getRuntimePath() . 'cert');
$param = [
'CertificateId' => $order['CertificateId'],
'ServiceType' => 'nginx',
];
$data = $this->request('DescribeDownloadCertificateUrl', $param);
$file_data = get_curl($data['DownloadCertificateUrl']);
$file_data = curl_client($data['DownloadCertificateUrl'], null, null, null, null, $this->proxy);
$file_data = $file_data['body'] ?? null;
if (empty($file_data)) throw new Exception('下载证书失败');
$file_path = app()->getRuntimePath() . 'cert/' . $data['DownloadFilename'];
$file_name = substr($data['DownloadFilename'], 0, -4);
file_put_contents($file_path, $file_data);

View File

@@ -80,7 +80,8 @@ class ucloud implements CertInterface
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']];
$name = substr($auth['AuthKey'], 0, -(strlen($mainDomain) + 1));
$dnsList[$mainDomain][] = ['name' => $name, 'type' => $auth['AuthType'] == 'DNS_TXT' ? 'TXT' : 'CNAME', 'value' => $auth['AuthValue']];
}
}
return $dnsList;

View File

@@ -16,7 +16,7 @@ class zerossl implements CertInterface
public function __construct($config, $ext = null)
{
$this->config = $config;
$this->ac = new ACMECert($this->directory, $config['proxy'] == 1);
$this->ac = new ACMECert($this->directory, (int)$config['proxy']);
if ($ext) {
$this->ext = $ext;
$this->ac->loadAccountKey($ext['key']);
@@ -27,20 +27,27 @@ class zerossl implements CertInterface
public function register()
{
if (empty($this->config['email'])) throw new Exception('邮件地址不能为空');
if (empty($this->config['kid']) || empty($this->config['key'])) throw new Exception('必填参数不能为空');
if (isset($this->config['eabMode']) && $this->config['eabMode'] == 'auto') {
$eab = $this->getEAB($this->config['email']);
} else {
$eab = ['kid' => $this->config['kid'], 'key' => $this->config['key']];
}
if (!empty($this->ext['key'])) {
$kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']);
$kid = $this->ac->registerEAB(true, $eab['kid'], $eab['key'], $this->config['email']);
return ['kid' => $kid, 'key' => $this->ext['key']];
}
$key = $this->ac->generateRSAKey(2048);
$this->ac->loadAccountKey($key);
$kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']);
$kid = $this->ac->registerEAB(true, $eab['kid'], $eab['key'], $this->config['email']);
return ['kid' => $kid, 'key' => $key];
}
public function buyCert($domainList, &$order) {}
public function buyCert($domainList, &$order)
{
}
public function createOrder($domainList, &$order, $keytype, $keysize)
{
@@ -57,7 +64,7 @@ class zerossl implements CertInterface
if (!empty($order['challenges'])) {
foreach ($order['challenges'] as $opts) {
$mainDomain = getMainDomain($opts['domain']);
$name = str_replace('.' . $mainDomain, '', $opts['key']);
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
/*if (!array_key_exists($mainDomain, $dnsList)) {
$dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "sectigo.com"'];
}*/
@@ -101,10 +108,27 @@ class zerossl implements CertInterface
$this->ac->revoke($pem);
}
public function cancel($order) {}
public function cancel($order)
{
}
public function setLogger($func)
{
$this->ac->setLogger($func);
}
private function getEAB($email)
{
$api = "https://api.zerossl.com/acme/eab-credentials-email";
$response = curl_client($api, http_build_query(['email' => $email]), null, null, null, $this->config['proxy'] == 1);
$result = json_decode($response['body'], true);
if (!isset($result['success'])) {
throw new Exception('获取EAB失败' . $response['body']);
} elseif (!$result['success'] && isset($result['error'])) {
throw new Exception('获取EAB失败' . $result['error']['code'] . ' - ' . $result['error']['type']);
} elseif (!isset($result['eab_kid']) || !isset($result['eab_hmac_key'])) {
throw new Exception('获取EAB失败返回数据不完整');
}
return ['kid' => $result['eab_kid'], 'key' => $result['eab_hmac_key']];
}
}

View File

@@ -145,6 +145,7 @@ class AWS
$path = '/' . $this->version . $path;
$body = '';
$query = [];
if ($method == 'GET' || $method == 'DELETE') {
$query = $params;
} else {
@@ -284,8 +285,9 @@ class AWS
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($etag) {
@@ -327,16 +329,31 @@ class AWS
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
}
private function array2xml($array, $xml = null)
private function array2xml($array, $xml = null, $parentTagName = 'root')
{
if ($xml === null) {
$xml = new \SimpleXMLElement('<root/>');
}
foreach ($array as $key => $value) {
// 确定当前标签名:如果是数字键名,使用父级标签名,否则使用当前键名
$tagName = is_numeric($key) ? $parentTagName : $key;
if (is_array($value)) {
$subNode = $xml->addChild($key);
$this->array2xml($value, $subNode);
// 检查数组的第一个子节点的键是否为0
$firstKey = array_key_first($value);
$isFirstKeyZero = ($firstKey === 0 || $firstKey === '0');
if ($isFirstKeyZero) {
// 如果第一个子节点的键是0则不生成当前节点标签直接递归子节点
$this->array2xml($value, $xml, $tagName);
} else {
// 否则生成当前节点标签,并递归子节点
$subNode = $xml->addChild($tagName);
$this->array2xml($value, $subNode, $tagName);
}
} else {
$xml->addChild($key, $value);
}

View File

@@ -62,8 +62,9 @@ class Aliyun
$errno = curl_errno($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
curl_close($ch);

View File

@@ -168,8 +168,9 @@ class AliyunNew
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

View File

@@ -119,8 +119,9 @@ class AliyunOSS
$errno = curl_errno($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
curl_close($ch);

View File

@@ -158,8 +158,9 @@ class BaiduCloud
$errno = curl_errno($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
curl_close($ch);

View File

@@ -144,8 +144,9 @@ class Ctyun
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
curl_close($ch);

View File

@@ -163,8 +163,9 @@ class HuaweiCloud
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

View File

@@ -167,8 +167,9 @@ class Jdcloud
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

View File

@@ -121,8 +121,9 @@ class Qiniu
$errno = curl_errno($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
curl_close($ch);

View File

@@ -113,8 +113,9 @@ class TencentCloud
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
curl_close($ch);

View File

@@ -218,8 +218,9 @@ class Volcengine
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception('Curl error: ' . curl_error($ch));
throw new Exception('Curl error: ' . $errmsg);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

View File

@@ -236,7 +236,7 @@ class aliyun implements DeployInterface
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);
$client->addBucketCnameCert($config['oss_bucket'], $config['domain'], $cert_id . '-cn-hangzhou');
$this->log('OSS域名 ' . $config['domain'] . ' 部署证书成功!');
}

View File

@@ -1,146 +0,0 @@
<?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);
$cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
if (!empty($list)) {
foreach ($list as $row) {
$params = [
'sslCertId' => $row['id'],
'isOn' => true,
'name' => $row['name'],
'description' => $row['description'],
'serverName' => $row['serverName'],
'isCA' => false,
'certData' => base64_encode($fullchain),
'keyData' => base64_encode($privatekey),
'timeBeginAt' => $certInfo['validFrom_time_t'],
'timeEndAt' => $certInfo['validTo_time_t'],
'dnsNames' => $domains,
'commonNames' => [$certInfo['issuer']['CN']],
];
$this->request('/SSLCertService/updateSSLCert', $params);
$this->log('证书ID:' . $row['id'] . '更新成功!');
}
} else {
$params = [
'isOn' => true,
'name' => $cert_name,
'description' => $cert_name,
'serverName' => $certInfo['subject']['CN'],
'isCA' => false,
'certData' => base64_encode($fullchain),
'keyData' => base64_encode($privatekey),
'timeBeginAt' => $certInfo['validFrom_time_t'],
'timeEndAt' => $certInfo['validTo_time_t'],
'dnsNames' => $domains,
'commonNames' => [$certInfo['issuer']['CN']],
];
$result = $this->request('/SSLCertService/createSSLCert', $params);
$this->log('证书ID:' . $result['sslCertId'] . '添加成功!');
}
}
private function getAccessToken()
{
$path = '/APIAccessTokenService/getAPIAccessToken';
$params = [
'type' => $this->usertype,
'accessKeyId' => $this->accessKeyId,
'accessKey' => $this->accessKey,
];
$result = $this->request($path, $params);
if (isset($result['token'])) {
$this->accessToken = $result['token'];
} else {
throw new Exception('登录成功获取AccessToken失败');
}
}
private function request($path, $params = null)
{
$url = $this->url . $path;
$headers = [];
$body = null;
if ($this->accessToken) {
$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);
}
}
}

View File

@@ -29,22 +29,24 @@ class aws implements DeployInterface
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
if ($config['product'] == 'acm') {
if (empty($config['acm_arn'])) throw new Exception('ACM ARN不能为空');
$this->get_cert_id($fullchain, $privatekey, $config['acm_arn'], true);
} else {
$this->deploy_cloudfront($fullchain, $privatekey, $config, $info);
}
}
private function deploy_cloudfront($fullchain, $privatekey, $config, &$info)
{
if (empty($config['distribution_id'])) throw new Exception('分配ID不能为空');
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
$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);
}
$cert_id = isset($info['cert_id']) ? $info['cert_id'] : null;
$cert_id = $this->get_cert_id($fullchain, $privatekey, $cert_id);
usleep(500000);
$client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'cloudfront.amazonaws.com', 'cloudfront', '2020-05-31', 'us-east-1', $this->proxy);
try {
@@ -54,20 +56,71 @@ class aws implements DeployInterface
}
$data['ViewerCertificate']['ACMCertificateArn'] = $cert_id;
$data['ViewerCertificate']['CloudFrontDefaultCertificate'] = false;
$xml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><DistributionConfig></DistributionConfig>');
$data['ViewerCertificate']['CloudFrontDefaultCertificate'] = 'false';
unset($data['ViewerCertificate']['Certificate']);
unset($data['ViewerCertificate']['CertificateSource']);
$xml = new \SimpleXMLElement('<DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/"></DistributionConfig>');
$client->requestXmlN('PUT', '/distribution/' . $config['distribution_id'] . '/config', $data, $xml);
$this->log('分配ID: ' . $config['distribution_id'] . ' 证书部署成功!');
}
private function get_cert_id($fullchain, $privatekey)
private function get_cert_id($fullchain, $privatekey, $cert_id = null, $acm = false)
{
$cert = explode('-----END CERTIFICATE-----', $fullchain)[0] . '-----END CERTIFICATE-----';
if ($acm === true && $cert_id == null) {
throw new Exception('ACM ARN不能为空');
}
$certificates = explode('-----END CERTIFICATE-----', $fullchain);
$cert = $certificates[0] . '-----END CERTIFICATE-----';
$client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'acm.us-east-1.amazonaws.com', 'acm', '', 'us-east-1', $this->proxy);
if (!empty($cert_id)) {
try {
$data = $client->request('POST', 'CertificateManager.GetCertificate', [
'CertificateArn' => $cert_id
]);
// 如果成功获取证书信息说明证书存在直接返回cert_id
if (isset($data['Certificate']) && trim($data['Certificate']) == trim($cert)) {
$this->log('证书已是最新ACM ARN' . $cert_id);
return $cert_id;
} else {
$this->log('证书已过期或被删除,准备更新或者重新上传');
}
} catch (Exception $e) {
if ($acm === true) {
throw new Exception('获取证书信息失败请检查ACM ARN是否正确' . $e->getMessage());
}
$this->log('证书已被删除:' . $cert_id. ',准备重新上传');
}
}
$certificateChain = '';
if (count($certificates) > 1) {
// 从第二个证书开始,重新拼接中间证书链
for ($i = 1; $i < count($certificates); $i++) {
if (trim($certificates[$i]) !== '') { // 忽略空字符串(可能由末尾分割产生)
$certificateChain .= $certificates[$i] . '-----END CERTIFICATE-----';
}
}
}
$param = [
'Certificate' => base64_encode($cert),
'PrivateKey' => base64_encode($privatekey),
];
// 如果有中间证书链,则添加到参数中
if (!empty($certificateChain)) {
$param['CertificateChain'] = base64_encode($certificateChain);
}
// 如果是ACM则添加ARN参数用于更新证书
if ($acm === true) {
$param['CertificateArn'] = $cert_id;
}
$client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'acm.us-east-1.amazonaws.com', 'acm', '', 'us-east-1', $this->proxy);
try {
$data = $client->request('POST', 'CertificateManager.ImportCertificate', $param);
@@ -75,6 +128,11 @@ class aws implements DeployInterface
} catch (Exception $e) {
throw new Exception('上传证书失败:' . $e->getMessage());
}
$this->log('证书上传成功:' . $cert_id);
$info['cert_id'] = $cert_id;
return $cert_id;
}

View File

@@ -46,7 +46,16 @@ class btpanel implements DeployInterface
foreach ($sites as $site) {
$siteName = trim($site);
if (empty($siteName)) continue;
if ($config['type'] == '2') {
if ($config['type'] == '3') {
try {
$this->deployDocker($siteName, $fullchain, $privatekey);
$this->log("Docker域名 {$siteName} 证书部署成功");
$success++;
} catch (Exception $e) {
$errmsg = $e->getMessage();
$this->log("Docker域名 {$siteName} 证书部署失败:" . $errmsg);
}
} elseif ($config['type'] == '2') {
try {
$this->deployMailSys($siteName, $fullchain, $privatekey);
$this->log("邮局域名 {$siteName} 证书部署成功");
@@ -129,6 +138,25 @@ class btpanel implements DeployInterface
}
}
private function deployDocker($domain, $fullchain, $privatekey)
{
$path = '/mod/docker/com/set_ssl';
$data = [
'site_name' => $domain,
'key' => $privatekey,
'csr' => $fullchain,
];
$response = $this->request($path, $data);
$result = json_decode($response, true);
if (isset($result['status']) && $result['status']) {
return true;
} elseif (isset($result['msg'])) {
throw new Exception($result['msg']);
} else {
throw new Exception($response ? $response : '返回数据解析失败');
}
}
public function setLogger($func)
{
$this->logger = $func;

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

@@ -0,0 +1,134 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class btwaf implements DeployInterface
{
private $logger;
private $url;
private $key;
private $proxy;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->key = $config['key'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥');
$path = '/api/user/latest_version';
$response = $this->request($path, []);
$result = json_decode($response, true);
if (isset($result['code']) && $result['code'] == 0) {
return true;
} else {
throw new Exception(isset($result['res']) ? $result['res'] : '面板地址无法连接');
}
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$sites = explode("\n", $config['sites']);
$success = 0;
$errmsg = null;
foreach ($sites as $site) {
$siteName = trim($site);
if (empty($siteName)) continue;
try {
$this->deploySite($siteName, $fullchain, $privatekey);
$this->log("网站 {$siteName} 证书部署成功");
$success++;
} catch (Exception $e) {
$errmsg = $e->getMessage();
$this->log("网站 {$siteName} 证书部署失败:" . $errmsg);
}
}
if ($success == 0) {
throw new Exception($errmsg ? $errmsg : '要部署的网站不存在');
}
}
private function deploySite($siteName, $fullchain, $privatekey)
{
$site_id = null;
$listen_ssl_port = ['443'];
$path = '/api/wafmastersite/get_site_list';
$data = ['p' => 1, 'p_size' => 10, 'site_name' => $siteName];
$response = $this->request($path, $data);
$result = json_decode($response, true);
if (isset($result['code']) && $result['code'] == 0) {
foreach ($result['res']['list'] as $site) {
if ($site['site_name'] == $siteName) {
$site_id = $site['site_id'];
if (isset($site['server']['listen_ssl_port']) && !empty($site['server']['listen_ssl_port'])) {
$listen_ssl_port = $site['server']['listen_ssl_port'];
}
break;
}
}
if (!$site_id) {
throw new Exception("网站名称不存在");
}
} elseif (isset($result['res'])) {
throw new Exception($result['res']);
} else {
throw new Exception($response ? $response : '返回数据解析失败');
}
$path = '/api/wafmastersite/modify_site';
$data = [
'types' => 'openCert',
'site_id' => $site_id,
'server' => [
'listen_ssl_port' => $listen_ssl_port,
'ssl' => [
'is_ssl' => 1,
'private_key' => $privatekey,
'full_chain' => $fullchain,
],
]
];
$response = $this->request($path, $data);
$result = json_decode($response, true);
if (isset($result['code']) && $result['code'] == 0) {
return true;
} elseif (isset($result['res'])) {
throw new Exception($result['res']);
} else {
throw new Exception($response ? $response : '返回数据解析失败');
}
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
private function request($path, $params)
{
$url = $this->url . $path;
$now_time = time();
$headers = [
'waf_request_time: ' . $now_time,
'waf_request_token: ' . md5($now_time . md5($this->key)),
'Content-Type: application/json',
];
$post = $params ? json_encode($params) : null;
$response = curl_client($url, $post, null, null, $headers, $this->proxy, 'POST');
return $response['body'];
}
}

View File

@@ -28,8 +28,8 @@ class doge implements DeployInterface
public function deploy($fullchain, $privatekey, $config, &$info)
{
$domain = $config['domain'];
if (empty($domain)) throw new Exception('绑定的域名不能为空');
$domains = $config['domain'];
if (empty($domains)) throw new Exception('绑定的域名不能为空');
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
@@ -37,13 +37,14 @@ class doge implements DeployInterface
$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 . ' 绑定证书成功!');
foreach (explode(',', $domains) as $domain) {
$param = [
'id' => $cert_id,
'domain' => $domain,
];
$this->request('/cdn/cert/bind.json', $param);
$this->log('CDN域名 ' . $domain . ' 绑定证书成功!');
}
$info['cert_id'] = $cert_id;
}

View File

@@ -59,6 +59,7 @@ class lecdn implements DeployInterface
$path = '/prod-api/login';
$params = [
'email' => $this->email,
'username' => $this->email,
'password' => $this->password,
];
$result = $this->request($path, $params);

View File

@@ -14,7 +14,7 @@ class opanel implements DeployInterface
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->url = rtrim($config['url'], '/') . '/api/' . (isset($config['version']) ? $config['version'] : 'v1');
$this->key = $config['key'];
$this->proxy = $config['proxy'] == 1;
}
@@ -22,7 +22,7 @@ class opanel implements DeployInterface
public function check()
{
if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥');
$this->request('/api/v1/settings/search');
$this->request("/settings/search");
}
public function deploy($fullchain, $privatekey, $config, &$info)
@@ -30,9 +30,9 @@ class opanel implements DeployInterface
$domains = $config['domainList'];
if (empty($domains)) throw new Exception('没有设置要部署的域名');
$params = ['page'=>1, 'pageSize'=>500];
$params = ['page' => 1, 'pageSize' => 500];
try {
$data = $this->request('/api/v1/websites/ssl/search', $params);
$data = $this->request("/websites/ssl/search", $params);
$this->log('获取证书列表成功(total=' . $data['total'] . ')');
} catch (Exception $e) {
throw new Exception('获取证书列表失败:' . $e->getMessage());
@@ -45,7 +45,7 @@ class opanel implements DeployInterface
if (empty($row['primaryDomain'])) continue;
$cert_domains = [];
$cert_domains[] = $row['primaryDomain'];
if(!empty($row['domains'])) $cert_domains += explode(',', $row['domains']);
if (!empty($row['domains'])) $cert_domains += explode(',', $row['domains']);
$flag = false;
foreach ($cert_domains as $domain) {
if (in_array($domain, $domains)) {
@@ -62,7 +62,7 @@ class opanel implements DeployInterface
'description' => '',
];
try {
$this->request('/api/v1/websites/ssl/upload', $params);
$this->request('/websites/ssl/upload', $params);
$this->log("证书ID:{$row['id']}更新成功!");
$success++;
} catch (Exception $e) {
@@ -93,21 +93,21 @@ class opanel implements DeployInterface
{
$url = $this->url . $path;
$timestamp = time().'';
$timestamp = time() . '';
$token = md5('1panel' . $this->key . $timestamp);
$headers = [
'1Panel-Token: '.$token,
'1Panel-Timestamp: '.$timestamp
'1Panel-Token: ' . $token,
'1Panel-Timestamp: ' . $timestamp
];
$body = $params ? json_encode($params) : '{}';
if($body) $headers[] = 'Content-Type: application/json';
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){
if (isset($result['code']) && $result['code'] == 200) {
return isset($result['data']) ? $result['data'] : null;
}elseif(isset($result['message'])){
} elseif (isset($result['message'])) {
throw new Exception($result['message']);
}else{
} else {
throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
}
}

View File

@@ -29,8 +29,8 @@ class qiniu implements DeployInterface
public function deploy($fullchain, $privatekey, $config, &$info)
{
$domain = $config['domain'];
if (empty($domain)) throw new Exception('绑定的域名不能为空');
$domains = $config['domain'];
if (empty($domains)) throw new Exception('绑定的域名不能为空');
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
@@ -38,14 +38,16 @@ class qiniu implements DeployInterface
$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);
} elseif ($config['product'] == 'pili') {
$this->deploy_pili($config['pili_hub'], $domain, $cert_name);
} else {
throw new Exception('未知的产品类型');
foreach (explode(',', $domains) as $domain) {
if ($config['product'] == 'cdn') {
$this->deploy_cdn($domain, $cert_id);
} elseif ($config['product'] == 'oss') {
$this->deploy_oss($domain, $cert_id);
} elseif ($config['product'] == 'pili') {
$this->deploy_pili($config['pili_hub'], $domain, $cert_name);
} else {
throw new Exception('未知的产品类型');
}
}
$info['cert_id'] = $cert_id;
$info['cert_name'] = $cert_name;

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

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

View File

@@ -105,6 +105,13 @@ class tencent implements DeployInterface
}
$this->log('上传证书成功 CertificateId=' . $data['CertificateId']);
usleep(300000);
$param = [
'CertificateIds' => [$data['CertificateId']],
'SwitchStatus' => 1,
];
$this->client->request('ModifyCertificatesExpiringNotificationSwitch', $param);
return $data['CertificateId'];
}

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

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

View File

@@ -89,6 +89,9 @@ class CertDeployService
private function saveResult($status, $error = null, $retrytime = null)
{
$this->task['status'] = $status;
if (mb_strlen($error) > 300) {
$error = mb_strcut($error, 0, 300);
}
$update = ['status' => $status, 'error' => $error, 'retrytime' => $retrytime];
if ($status == 1){
$update['retry'] = 0;

View File

@@ -178,6 +178,9 @@ class CertOrderService
private function saveResult($status, $error = null, $retrytime = null)
{
$this->order['status'] = $status;
if (mb_strlen($error) > 300) {
$error = mb_strcut($error, 0, 300);
}
$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) {

View File

@@ -21,10 +21,14 @@ class CertTaskService
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 isauto=1 AND expiretime<:expiretime', ['expiretime' => date('Y-m-d H:i:s', time() + $days * 86400)])->select();
$list = Db::name('cert_order')->field('id,aid,status,issend')->whereRaw('status NOT IN (3,4) AND (retrytime IS NULL OR retrytime<NOW()) OR status=3 AND isauto=1 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) {
if ($row['aid'] == 0) {
if($row['issend'] == 0) MsgNotice::cert_order_send($row['id'], true);
continue;
}
try {
$service = new CertOrderService($row['id']);
if ($row['status'] == 3) {

View File

@@ -45,9 +45,9 @@ class TaskRunner
if ($row['checktype'] == 2) {
$result = CheckUtils::curl($row['checkurl'], $row['timeout'], $row['main_value'], $row['proxy'] == 1);
} elseif ($row['checktype'] == 1) {
$result = CheckUtils::tcp($row['main_value'], $row['tcpport'], $row['timeout']);
$result = CheckUtils::tcp($row['main_value'], $row['checkurl'], $row['tcpport'], $row['timeout']);
} else {
$result = CheckUtils::ping($row['main_value']);
$result = CheckUtils::ping($row['main_value'], $row['checkurl']);
}
$action = 0;

View File

@@ -9,6 +9,12 @@ class CheckUtils
$status = true;
$errmsg = null;
$urlarr = parse_url($url);
if (!$urlarr) {
return ['status' => false, 'errmsg' => 'Invalid URL', 'usetime' => 0];
}
if (substr($urlarr['host'], 0, 1) == '[' && substr($urlarr['host'], -1) == ']') {
$urlarr['host'] = substr($urlarr['host'], 1, -1);
}
if (!empty($ip) && !filter_var($urlarr['host'], FILTER_VALIDATE_IP)) {
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
$ip = gethostbyname($ip);
@@ -30,6 +36,8 @@ class CheckUtils
$proxy_type = CURLPROXY_SOCKS4;
} elseif ($proxy_type == 'sock5') {
$proxy_type = CURLPROXY_SOCKS5;
} elseif ($proxy_type == 'sock5h') {
$proxy_type = CURLPROXY_SOCKS5_HOSTNAME;
} else {
$proxy_type = CURLPROXY_HTTP;
}
@@ -72,8 +80,9 @@ class CheckUtils
return ['status' => $status, 'errmsg' => $errmsg, 'usetime' => $usetime];
}
public static function tcp($target, $port, $timeout)
public static function tcp($target, $ip, $port, $timeout)
{
if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP)) $target = $ip;
if (substr($target, -1) == '.') $target = substr($target, 0, -1);
if (!filter_var($target, FILTER_VALIDATE_IP) && checkDomain($target)) {
$target = gethostbyname($target);
@@ -95,9 +104,10 @@ class CheckUtils
return ['status' => $status, 'errmsg' => $errStr, 'usetime' => $usetime];
}
public static function ping($target)
public static function ping($target, $ip)
{
if (!function_exists('exec')) return ['status' => false, 'errmsg' => 'exec函数不可用', 'usetime' => 0];
if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP)) $target = $ip;
if (substr($target, -1) == '.') $target = substr($target, 0, -1);
if (!filter_var($target, FILTER_VALIDATE_IP) && checkDomain($target)) {
$target = gethostbyname($target);

View File

@@ -2,6 +2,8 @@
namespace app\utils;
use Exception;
class DnsQueryUtils
{
private static $doh_servers = ['https://dns.alidns.com/resolve', 'https://doh.pub/resolve', 'https://doh.360.cn/resolve'];
@@ -10,7 +12,11 @@ class DnsQueryUtils
{
$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]);
try{
$list = dns_get_record($domain, $dns_type[$type]);
}catch(Exception $e){
return false;
}
if (!$list || empty($list)) return false;
$result = [];
foreach ($list as $row) {

View File

@@ -66,24 +66,33 @@ class MsgNotice
{
$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 ($row['aid'] == 0) {
if (count($domainList) > 1) {
$mail_title = $domainList[0] . '等' . count($domainList) . '个域名SSL证书签发成功通知';
$mail_title = $domainList[0] . '等' . count($domainList) . '个域名SSL证书即将到期提醒';
} else {
$mail_title = $domainList[0] . '域名SSL证书签发成功通知';
$mail_title = $domainList[0] . '域名SSL证书即将到期提醒';
}
$mail_content = '尊敬的用户,您好:您SSL证书已签发成功!<br/><b>证书账户:</b> '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')<br/><b>证书域名:</b> '.implode('、', $domainList).'<br/><b>签发时间:</b> '.$row['issuetime'].'<br/><b>到期时间:</b> '.$row['expiretime'].'<br/><b>颁发机构:</b> '.$row['issuer'];
$mail_content = '尊敬的用户,您好:您有一张SSL证书将在'.config_get('cert_renewdays', 7).'天后到期,该证书为手动续期证书,请及时续期!<br/><b>证书域名:</b> '.implode('、', $domainList).'<br/><b>签发时间:</b> '.$row['issuetime'].'<br/><b>到期时间:</b> '.$row['expiretime'].'<br/><b>颁发机构:</b> '.$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']].'通知';
$type = Db::name('cert_account')->where('id', $row['aid'])->value('type');
if ($result) {
if (count($domainList) > 1) {
$mail_title = $domainList[0] . '等' . count($domainList) . '个域名SSL证书签发成功通知';
} else {
$mail_title = $domainList[0] . '域名SSL证书签发成功通知';
}
$mail_content = '尊敬的用户您好您的SSL证书已签发成功<br/><b>证书账户:</b> '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')<br/><b>证书域名:</b> '.implode('、', $domainList).'<br/><b>签发时间:</b> '.$row['issuetime'].'<br/><b>到期时间:</b> '.$row['expiretime'].'<br/><b>颁发机构:</b> '.$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/><b>证书账户:</b> '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')<br/><b>证书域名:</b> '.implode('、', $domainList).'<br/><b>失败时间:</b> '.date('Y-m-d H:i:s').'<br/><b>失败原因:</b> <font color="warning">'.$row['error'].'</font>';
}
$mail_content = '尊敬的用户您好您的SSL证书'.$status_arr[$row['status']].'<br/><b>证书账户:</b> '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')<br/><b>证书域名:</b> '.implode('、', $domainList).'<br/><b>失败时间:</b> '.date('Y-m-d H:i:s').'<br/><b>失败原因:</b> <font color="warning">'.$row['error'].'</font>';
}
$mail_content .= '<br/><font color="grey">'.self::$sitename.'</font><br/><font color="grey">'.date('Y-m-d H:i:s').'</font>';

View File

@@ -55,13 +55,13 @@ a{color:#444}
<div class="input-group-addon"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></div>
<input type="password" class="form-control" placeholder="密码" name="password" required="required"/>
</div>
<div class="input-group">
{if config_get('vcode', '1')=='1'}<div class="input-group">
<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="/verifycode" height="45" onclick="this.src='/verifycode?r='+Math.random();" title="点击更换验证码">
</span>
</div>
</div>{/if}
<div class="form-group">
<button type="submit" class="btn btn-success btn-lg btn-block" id="submit" style="background:#708eea;">登 录</button>
</div>

View File

@@ -9,19 +9,108 @@
color: #f56c6c;
margin-right: 4px;
}
/* 账户类型卡片样式 */
.account-type-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 20px;
}
.account-type-category {
width: 100%;
margin-bottom: 10px;
font-size: 18px;
font-weight: bold;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
.account-type-card {
width: calc(25% - 15px);
min-width: 200px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
background: #fff;
height: 100px;
overflow: hidden;
}
.account-type-card:hover {
border-color: #409EFF;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
.account-type-card .icon {
width: 30px;
margin: 11px 8px;
float: left;
}
.account-type-card .content {
margin-left: 38px;
}
.account-type-card .title {
font-size: 14px;
font-weight: bold;
margin-bottom: 3px;
color: #333;
}
.account-type-card .desc {
font-size: 12px;
color: #999;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 768px) {
.account-type-card {
width: calc(50% - 15px);
}
}
@media (max-width: 480px) {
.account-type-card {
width: 100%;
height: 78px;
}
.account-type-card .desc {
-webkit-line-clamp: 1;
}
}
</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 id="account-type-view" v-if="!selectedType">
<div v-for="(category, classId) in groupedTypes" :key="classId">
<div class="account-type-category">{{ category.label }}</div>
<div class="account-type-container">
<div class="account-type-card" v-for="type in category.types" :key="type.value" @click="selectType(type.value)">
<img class="icon" :src="'/static/images/' + typeList[type.value].icon" :alt="type.label">
<div class="content">
<div class="title">{{ type.label }}</div>
<div class="desc">{{ typeList[type.value].desc || ''}}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 表单视图 -->
<form onsubmit="return false" method="post" class="form-horizontal" role="form" id="accountform" v-if="selectedType">
<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 class="form-control-static">
{{ typeList[set.type].name }}
<a href="javascript:;" @click="selectedType = false" class="pull-right btn btn-default" v-if="action=='add'">重新选择</a>
</div>
<input type="hidden" name="type" v-model="set.type">
</div>
</div>
<div v-for="(item,name) in inputs" v-show="isShow(item.show)">
@@ -104,6 +193,7 @@ new Vue({
el: '#app',
data: {
action: '{$action}',
selectedType: false,
set: {
deploy: '{$deploy}',
id: '',
@@ -140,16 +230,24 @@ new Vue({
}
}
},
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})
}
computed: {
groupedTypes() {
return 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], types: tempList}
})
return {label: classList[key], children: tempList}
})
}
},
mounted() {
this.typeOption = this.groupedTypes;
if(this.action == 'edit') {
this.selectedType = true;
}
if(this.action == 'edit'){
Object.keys(info).forEach((key) => {
this.set[key] = info[key]
@@ -181,6 +279,10 @@ new Vue({
})
},
methods: {
selectType(type) {
this.set.type = type;
this.selectedType = true;
},
submit(){
var that=this;
Object.keys(this.config).forEach((key) => {
@@ -235,4 +337,4 @@ new Vue({
},
});
</script>
{/block}
{/block}

View File

@@ -65,10 +65,10 @@ $(document).ready(function(){
title: '添加时间'
},
{
field: '',
field: 'action',
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>';
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> <a href="/cert/certorder?aid='+row.id+'" class="btn btn-default btn-xs">订单</a>';
return html;
}
},
@@ -79,12 +79,12 @@ function delItem(id){
layer.confirm('确定要删除此账户吗?', {
btn: ['确定','取消']
}, function(){
$.post('/cert/account/del?deploy=0', {id: id}, function(data){
$.post('/cert/account/del', {id: id, deploy: 0}, function(data){
if(data.code == 0) {
layer.msg('删除成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
layer.alert(data.msg, {icon: 2});
}
}, 'json');
});

View File

@@ -16,6 +16,7 @@ pre.pre-log{height: 330px;overflow-y: auto;width: 100%;background-color: rgba(51
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<input type="hidden" name="id" value="">
<input type="hidden" name="aid" value="">
<div class="form-group">
<label>搜索</label>
<div class="form-group">
@@ -34,12 +35,6 @@ pre.pre-log{height: 330px;overflow-y: auto;width: 100%;background-color: rgba(51
<a href="javascript:searchClear()" class="btn btn-default" title="刷新订单列表"><i class="fa fa-refresh"></i> 刷新</a>
<div class="btn-group">
<a href="/cert/order/add" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="/cert/order/import">导入已有证书</a></li>
</ul>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">批量操作 <span class="caret"></span></button>
@@ -87,7 +82,10 @@ $(document).ready(function(){
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>';
if(value){
return '<span title="'+row.aremark+'" data-toggle="tooltip" data-placement="right"><img src="/static/images/'+row.icon+'" class="type-logo">'+value+'('+row.aid+')</span>';
}
return '手动续期';
}
},
{
@@ -197,7 +195,7 @@ $(document).ready(function(){
}
},
{
field: '',
field: 'action',
title: '操作',
formatter: function(value, row, index) {
var html = '';
@@ -208,7 +206,10 @@ $(document).ready(function(){
}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;';
html += '<a href="javascript:download(\''+row.id+'\')" class="btn btn-success btn-xs"><i class="fa fa-download"></i> 下载</a>&nbsp;&nbsp;';
if(row.aid > 0){
html += '<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{
@@ -219,7 +220,9 @@ $(document).ready(function(){
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>';
if(row.aid > 0){
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){

View File

@@ -93,7 +93,7 @@ function saveSetting(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '',
url : '/system/set',
data : $(obj).serialize(),
dataType : 'json',
success : function(data) {

View File

@@ -130,7 +130,7 @@ $(document).ready(function(){
title: '添加时间'
},
{
field: '',
field: 'action',
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>';

View File

@@ -65,10 +65,10 @@ $(document).ready(function(){
title: '添加时间'
},
{
field: '',
field: 'action',
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>';
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> <a href="/cert/deploytask?aid='+row.id+'" class="btn btn-default btn-xs">任务</a>';
return html;
}
},
@@ -79,12 +79,12 @@ function delItem(id){
layer.confirm('确定要删除此账户吗?', {
btn: ['确定','取消']
}, function(){
$.post('/cert/account/del?deploy=1', {id: id}, function(data){
$.post('/cert/account/del', {id: id, deploy: 1}, function(data){
if(data.code == 0) {
layer.msg('删除成功', {icon: 1, time:800});
$('#listTable').bootstrapTable('refresh');
} else {
layer.msg(data.msg, {icon: 2});
layer.alert(data.msg, {icon: 2});
}
}, 'json');
});

View File

@@ -12,6 +12,7 @@ pre.pre-log{height: 330px;overflow-y: auto;width: 100%;background-color: rgba(51
<div class="panel-body">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<input type="hidden" name="aid" value="">
<input type="hidden" name="oid" value="">
<div class="form-group">
<label>搜索</label>
@@ -37,7 +38,7 @@ pre.pre-log{height: 330px;overflow-y: auto;width: 100%;background-color: rgba(51
<a href="/cert/deploy/add" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">批量操作 <span class="caret"></span></button>
<ul class="dropdown-menu"><li><a href="javascript:operation('delete')">删除</a></li><li><a href="javascript:operation('reset')">重置任务</a></li><li><a href="javascript:operation('open')">开启任务</a></li><li><a href="javascript:operation('close')">停止任务</a></li></ul>
<ul class="dropdown-menu"><li><a href="javascript:operation('delete')">删除</a></li><li><a href="javascript:operation('reset')">重置任务</a></li><li><a href="javascript:operation('open')">开启任务</a></li><li><a href="javascript:operation('close')">停止任务</a></li><li><a href="javascript:operation('cert')">修改关联证书</a></li></ul>
</div>
</form>
@@ -80,6 +81,7 @@ $(document).ready(function(){
field: 'typename',
title: '自动部署账户',
formatter: function(value, row, index) {
if(!value) return '已被删除'
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>';
}
},
@@ -130,7 +132,7 @@ $(document).ready(function(){
}
},
{
field: '',
field: 'action',
title: '操作',
formatter: function(value, row, index) {
var html = '';
@@ -307,6 +309,8 @@ function operation(action){
if(!confirm('确定要删除所选自动部署任务吗?')) return;
}else if(action == 'reset'){
if(!confirm('重置任务后,任务将变成待处理状态,是否确定重置?')) return;
}else if(action == 'cert'){
return batch_set_cert(ids);
}
var ii = layer.load(2);
@@ -327,5 +331,26 @@ function operation(action){
}
});
}
function batch_set_cert(ids){
layer.prompt({title: '填写证书ID', value: '', formType: 0}, function(text, index){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/cert/deploy/operation',
data : {action: 'cert', ids: ids, certid: text},
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.closeAll();
layer.alert(data.msg, {icon: 1});
searchRefresh();
}else{
layer.alert(data.msg, {icon: 2});
}
}
});
});
}
</script>
{/block}

View File

@@ -22,9 +22,28 @@
{foreach $accounts as $k=>$v}
<option value="{$k}" data-type="{$v.type}">{$v.name}</option>
{/foreach}
<option value="-1" data-type="">手动续期</option>
</select></div>
</div>
<div class="form-group">
<div class="form-group" v-show="set.aid==-1">
<label class="col-sm-3 control-label no-padding-right" is-required>证书内容</label>
<div class="col-sm-6">
<div class="input-group">
<textarea name="fullchain" v-model="set.fullchain" class="form-control" rows="5" placeholder="输入PEM格式证书链" required></textarea>
<a class="btn btn-default input-group-addon" @click="upload('fullchain')" title="上传证书文件"><i class="fa fa-upload"></i></a>
</div>
</div>
</div>
<div class="form-group" v-show="set.aid==-1">
<label class="col-sm-3 control-label no-padding-right" is-required>私钥内容</label>
<div class="col-sm-6">
<div class="input-group">
<textarea name="privatekey" v-model="set.privatekey" class="form-control" rows="5" placeholder="输入PEM格式私钥" required></textarea>
<a class="btn btn-default input-group-addon" @click="upload('privatekey')" title="上传私钥文件"><i class="fa fa-upload"></i></a>
</div>
</div>
</div>
<div class="form-group" v-show="set.aid!=-1">
<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">
@@ -32,7 +51,7 @@
</label>
</div>
</div>
<div class="form-group">
<div class="form-group" v-show="set.aid!=-1">
<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">
@@ -41,7 +60,7 @@
</div>
</div>
<div class="form-group">
<div class="form-group" v-show="set.aid!=-1">
<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>
@@ -51,7 +70,8 @@
<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>
<div class="panel panel-default" v-show="set.aid!=-1"><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>
<div class="panel panel-default" v-show="set.aid==-1"><div class="panel-body"><p><b style="color:#39b603;"><i class="fa fa-info-circle fa-fw"></i></b>提示:选择手动续期,到达设置的续期天数,只会发送消息通知。</p></div></div>
</form>
</div>
</div>
@@ -72,6 +92,8 @@ new Vue({
set: {
id: '',
aid: '',
fullchain: '',
privatekey: '',
keytype: '',
keysize: '',
domains: [],
@@ -150,6 +172,22 @@ new Vue({
layer.msg('服务器错误');
}
});
},
upload(name){
//读取上传文件并填充到表单
var file = document.createElement('input');
file.type = 'file';
file.accept = '.pem,.crt,.key';
file.style.display = 'none';
file.onchange = function(){
var reader = new FileReader();
reader.onload = function(e){
this.set[name] = e.target.result;
}.bind(this);
reader.readAsText(file.files[0]);
}.bind(this);
document.body.appendChild(file);
file.click();
}
},
});

View File

@@ -1,167 +0,0 @@
{extend name="common/layout" /}
{block name="title"}导入已有证书{/block}
{block name="main"}
<style>
.tips{color: #f6a838; padding-left: 5px;}
.control-label[is-required]:before {
content: "*";
color: #f56c6c;
margin-right: 4px;
}
.input-group-addon{padding: 6px 6px;}
</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>导入已有证书</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 control-label no-padding-right" is-required>证书内容</label>
<div class="col-sm-6">
<div class="input-group">
<textarea name="fullchain" v-model="set.fullchain" class="form-control" rows="5" placeholder="输入PEM格式证书链" required></textarea>
<a class="btn btn-default input-group-addon" @click="upload('fullchain')" title="上传证书文件"><i class="fa fa-upload"></i></a>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right" is-required>私钥内容</label>
<div class="col-sm-6">
<div class="input-group">
<textarea name="privatekey" v-model="set.privatekey" class="form-control" rows="5" placeholder="输入PEM格式私钥" required></textarea>
<a class="btn btn-default input-group-addon" @click="upload('privatekey')" title="上传私钥文件"><i class="fa fa-upload"></i></a>
</div>
</div>
</div>
<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">
<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>
new Vue({
el: '#app',
data: {
type: '',
set: {
fullchain: '',
privatekey: '',
aid: '',
keytype: '',
keysize: '',
},
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;
},
},
mounted() {
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('服务器错误');
}
});
},
upload(name){
//读取上传文件并填充到表单
var file = document.createElement('input');
file.type = 'file';
file.accept = '.pem,.crt,.key';
file.style.display = 'none';
file.onchange = function(){
var reader = new FileReader();
reader.onload = function(e){
this.set[name] = e.target.result;
}.bind(this);
reader.readAsText(file.files[0]);
}.bind(this);
document.body.appendChild(file);
file.click();
}
},
});
</script>
{/block}

View File

@@ -195,7 +195,7 @@ function submitClean(){
});
}
function submitNotice(){
$.post('/dmonitor/noticeset', $("#form-notice").serialize(), function(res){
$.post('/system/set', $("#form-notice").serialize(), function(res){
if(res.code == 0){
layer.alert('设置保存成功!<br/>重启检测进程或容器后生效', {
icon: 1,

View File

@@ -134,7 +134,7 @@ $(document).ready(function(){
}
},
{
field: '',
field: 'action',
title: '操作',
formatter: function(value, row, index) {
var html = '<a href="/dmonitor/task/info/'+row.id+'" class="btn btn-info btn-xs">切换日志</a>&nbsp;&nbsp;';

View File

@@ -7,6 +7,11 @@
position: absolute;
left: 0;
}
.control-label[is-required]:before {
content: "*";
color: #f56c6c;
margin-right: 4px;
}
.tips{color: #f6a838; padding-left: 5px;}
</style>
<div class="row" id="app">
@@ -16,7 +21,7 @@
<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">域名选择</label>
<label class="col-sm-3 col-xs-12 control-label no-padding-right" is-required>域名选择</label>
<div class="col-sm-6">
<div class="input-group">
<input type="text" name="rr" v-model="set.rr" placeholder="主机记录" class="form-control" required>
@@ -29,7 +34,7 @@
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">解析记录</label>
<label class="col-sm-3 control-label no-padding-right" is-required>解析记录</label>
<div class="col-sm-6"><div class="input-group">
<select name="recordid" v-model="set.recordid" id="recordid" class="form-control" required>
<option v-for="option in recordList" :value="option.RecordId">{{option.Value}} (线路:{{option.LineName}})</option>
@@ -40,7 +45,7 @@
</div></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">切换设置</label>
<label class="col-sm-3 control-label no-padding-right" is-required>切换设置</label>
<div class="col-sm-6">
<label class="radio-inline" v-for="option in typeList">
<input type="radio" name="type" :value="option.value" v-model="set.type" :disabled="option.disabled"> {{option.label}}
@@ -48,7 +53,7 @@
</div>
</div>
<div class="form-group" v-show="set.type==2">
<label class="col-sm-3 control-label no-padding-right">备用解析记录</label>
<label class="col-sm-3 control-label no-padding-right" is-required>备用解析记录</label>
<div class="col-sm-6">
<input type="text" name="backup_value" v-model="set.backup_value" placeholder="支持填写IPv4或CNAME地址" class="form-control" required>
</div>
@@ -63,21 +68,27 @@
</div>
</div>
<div class="form-group" v-show="set.type<=2">
<label class="col-sm-3 control-label no-padding-right">检测协议</label>
<label class="col-sm-3 control-label no-padding-right" is-required>检测协议</label>
<div class="col-sm-6">
<label class="radio-inline" v-for="option in checktypeList">
<input type="radio" name="checktype" :value="option.value" v-model="set.checktype" :disabled="option.disabled"> {{option.label}}
</label>
</div>
</div>
<div class="form-group" v-show="set.type<=2&&set.checktype<2">
<label class="col-sm-3 control-label no-padding-right">指定检测IP</label>
<div class="col-sm-6">
<input type="text" name="checkip" v-model="set.checkurl" placeholder="留空默认为解析记录值IP" class="form-control" data-bv-ip="true">
</div>
</div>
<div class="form-group" v-show="set.type<=2&&set.checktype==1">
<label class="col-sm-3 control-label no-padding-right">TCP检测端口</label>
<label class="col-sm-3 control-label no-padding-right" is-required>TCP检测端口</label>
<div class="col-sm-6">
<input type="text" name="tcpport" v-model="set.tcpport" placeholder="填写TCP端口号" class="form-control" data-bv-integer="true" min="1" max="65535" required>
</div>
</div>
<div class="form-group" v-show="set.type<=2&&set.checktype==2">
<label class="col-sm-3 control-label no-padding-right">检测URL地址</label>
<label class="col-sm-3 control-label no-padding-right" is-required>检测URL地址</label>
<div class="col-sm-6">
<input type="text" name="checkurl" v-model="set.checkurl" placeholder="填写以http(s)://开头的完整地址http状态码须为2xx/3xx" class="form-control" data-bv-uri="true" required>
</div>
@@ -94,7 +105,7 @@
</div>
</div>
<div class="form-group" v-show="set.type<=2&&set.checktype>0">
<label class="col-sm-3 control-label no-padding-right">最大超时时间</label>
<label class="col-sm-3 control-label no-padding-right" is-required>最大超时时间</label>
<div class="col-sm-3">
<div class="input-group">
<input type="text" name="timeout" v-model="set.timeout" placeholder="填写请求最大超时时间" class="form-control" data-bv-integer="true" min="1" required>
@@ -103,13 +114,13 @@
</div>
</div>
<div class="form-group" v-show="set.type==3">
<label class="col-sm-3 control-label no-padding-right">同域名正常数量<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="与暂停解析配合使用,当同域名正常记录数量&lt;=几条时开启解析"><i class="fa fa-question-circle"></i></span></label>
<label class="col-sm-3 control-label no-padding-right" is-required>同域名正常数量<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="与暂停解析配合使用,当同域名正常记录数量&lt;=几条时开启解析"><i class="fa fa-question-circle"></i></span></label>
<div class="col-sm-3">
<input type="text" name="cycle" v-model="set.cycle" placeholder="同域名正常记录数量&lt;=几条时开启解析" class="form-control" data-bv-integer="true" min="0" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">检测间隔</label>
<label class="col-sm-3 control-label no-padding-right" is-required>检测间隔</label>
<div class="col-sm-3">
<div class="input-group">
<input type="text" name="frequency" v-model="set.frequency" placeholder="每次检测的间隔时间" class="form-control" data-bv-integer="true" min="1" required>
@@ -118,7 +129,7 @@
</div>
</div>
<div class="form-group" v-show="set.type<=2">
<label class="col-sm-3 control-label no-padding-right">确认次数</label>
<label class="col-sm-3 control-label no-padding-right" is-required>确认次数</label>
<div class="col-sm-3">
<input type="text" name="cycle" v-model="set.cycle" placeholder="连续失败几次后进行切换" class="form-control" data-bv-integer="true" min="1" required>
</div>

View File

@@ -130,7 +130,7 @@ $(document).ready(function(){
title: '添加时间'
},
{
field: '',
field: 'action',
title: '操作',
formatter: function(value, row, index) {
var html = '<a href="javascript:editframe('+row.id+')" class="btn btn-info btn-xs">编辑</a> <a href="javascript:delItem('+row.id+')" class="btn btn-danger btn-xs">删除</a>';

View File

@@ -37,6 +37,17 @@ tbody tr>td:nth-child(3){min-width:300px;word-break:break-all;}
<div class="col-sm-6">
<select name="line" class="form-control" disabled><option value="default">默认</option></select>
</div>
</div>
<div class="form-group" v-if="existCF">
<label class="col-sm-3 control-label no-padding-right">开启反代</label>
<div class="col-sm-6">
<label class="radio-inline">
<input type="radio" name="proxy" value="0" v-model="set.proxy">
</label>
<label class="radio-inline">
<input type="radio" name="proxy" value="1" v-model="set.proxy">仅Cloudflare域名
</label>
</div>
</div>
<div class="form-group" style="display:none" id="mx_type">
<label class="col-sm-3 control-label no-padding-right">MX优先级</label>
@@ -91,7 +102,9 @@ new Vue({
type: '',
mx: 10,
ttl: 600,
}
proxy: 0,
},
existCF: false,
},
watch: {
'set.type': function(val){
@@ -112,6 +125,7 @@ new Vue({
for(var i=0; i<this.domainList.length; i++){
this.$set(this.domainList[i], 'result', '<span class="text-muted">待添加</span>');
}
this.existCF = this.domainList.some(item => item.type === 'cloudflare');
},
methods: {
async save(id){

View File

@@ -40,7 +40,7 @@ tbody tr>td:nth-child(3){min-width:300px;word-break:break-all;}
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">记录值</label>
<div class="col-sm-6">
<input type="text" class="form-control" name="value" v-model="set.value" placeholder="输入记录值" required>
<input type="text" class="form-control" name="value" v-model="set.value" placeholder="输入新的记录值" required>
</div>
</div>
<div class="form-group" style="display:none" id="mx_type">

View File

@@ -1,6 +1,7 @@
{extend name="common/layout" /}
{block name="title"}域名管理{/block}
{block name="main"}
<link href="{$cdnpublic}bootstrap-datetimepicker/4.17.47/css/bootstrap-datetimepicker.min.css" rel="stylesheet">
<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">
@@ -50,6 +51,12 @@
<div class="modal-body">
<form class="form-horizontal" id="form-store2">
<input type="hidden" name="id"/>
<div class="form-group">
<label class="col-sm-3 control-label">到期时间</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="expiretime" placeholder="" value="">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">到期提醒</label>
<div class="col-sm-9">
@@ -137,6 +144,9 @@
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}moment.js/2.29.4/moment.min.js"></script>
<script src="{$cdnpublic}moment.js/2.29.4/locale/zh-cn.js"></script>
<script src="{$cdnpublic}bootstrap-datetimepicker/4.17.47/js/bootstrap-datetimepicker.min.js"></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>
@@ -261,7 +271,7 @@ $(document).ready(function(){
title: '备注'
},
{
field: '',
field: 'action',
title: '操作',
formatter: function(value, row, index) {
var html = '<a href="/record/'+row.id+'" class="btn btn-success btn-xs" onclick="loading()">解析</a>';
@@ -334,6 +344,12 @@ function editframe(id){
$("#form-store2 select[name=is_sso]").val(row.is_sso);
$("#form-store2 select[name=is_notice]").val(row.is_notice);
$("#form-store2 input[name=remark]").val(row.remark);
$("#form-store2 input[name=expiretime]").datetimepicker({
format: 'YYYY-MM-DD HH:mm:ss',
locale: 'zh-cn',
defaultDate: row.expiretime,
}).val(row.expiretime);
}
function saveEdit(){
var ii = layer.load(2);
@@ -549,4 +565,4 @@ document.addEventListener("visibilitychange", function() {
}
});
</script>
{/block}
{/block}

View File

@@ -316,7 +316,7 @@ $(document).ready(function(){
}
},
{
field: '',
field: 'action',
title: '操作',
formatter: function(value, row, index) {
if((row.Type == 'NS' || row.Type == 'SOA') && row.Name == '@') return '-';

View File

@@ -70,6 +70,19 @@
</div>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">其他登录设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveAccount(this)" method="post" class="form-horizontal" role="form">
<div class="form-group">
<div class="form-group">
<label class="col-sm-3 control-label">开启图形验证码</label>
<div class="col-sm-9" style="margin-top:6.5px"><div class="material-switch"><input id="vocde_switch" type="checkbox" {if config_get('vcode', '1')=='1'}checked{/if} onchange="setvcode()"><label for="vocde_switch" class="label-primary"></label></div></div>
</div>
</div>
</form>
</div>
</div>
{/block}
{block name="script"}
<script src="{$cdnpublic}layer/3.1.1/layer.js"></script>
@@ -168,6 +181,17 @@ function close_totp(){
});
});
}
function setvcode(){
var status = $("#vocde_switch").is(':checked') ? '1' : '2';
$.post('/system/set', {vcode: status}, function(res){
if(res.code == 0){
layer.msg(status == '1' ? '图形验证码已开启' : '图形验证码已关闭', {icon: 1, time: 1000});
}else{
layer.alert(res.msg, {icon: 2});
$("#vocde_switch").prop('checked', !status);
}
});
}
$(document).ready(function(){
var clipboard = new Clipboard('#copy-btn');
clipboard.on('success', function (e) {

View File

@@ -131,7 +131,7 @@ $(document).ready(function(){
}
},
{
field: '',
field: 'action',
title: '操作',
formatter: function(value, row, index) {
var html = '<a href="javascript:runTask(\''+row.id+'\')" class="btn btn-success btn-xs">手动更新</a>&nbsp;&nbsp;';

View File

@@ -165,7 +165,7 @@ function saveSetting(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '',
url : '/system/set',
data : $(obj).serialize(),
dataType : 'json',
success : function(data) {

View File

@@ -30,6 +30,7 @@
<option value="https">HTTPS</option>
<option value="sock4">SOCK4</option>
<option value="sock5">SOCK5</option>
<option value="sock5h">SOCK5H</option>
</select></div>
</div><br/>
<div class="form-group">
@@ -56,7 +57,7 @@ function saveSetting(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '',
url : '/system/set',
data : $(obj).serialize(),
dataType : 'json',
success : function(data) {

View File

@@ -174,7 +174,7 @@ $(document).ready(function(){
}
},
{
field: '',
field: 'action',
title: '操作',
formatter: function(value, row, index) {
var html = '<a href="javascript:editframe('+row.id+')" class="btn btn-info btn-xs">编辑</a> <a href="javascript:delItem('+row.id+')" class="btn btn-danger btn-xs">删除</a>';

View File

@@ -31,7 +31,7 @@ return [
'show_error_msg' => true,
'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl',
'version' => '1033',
'version' => '1036',
'dbversion' => '1033'
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/static/images/bt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/static/images/ratpanel.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

View File

@@ -80,7 +80,6 @@ Route::group(function () {
Route::get('/dmonitor/task/info/:id', 'dmonitor/taskinfo');
Route::any('/dmonitor/task/:action', 'dmonitor/taskform');
Route::get('/dmonitor/task', 'dmonitor/task');
Route::post('/dmonitor/noticeset', 'dmonitor/noticeset');
Route::post('/dmonitor/clean', 'dmonitor/clean');
Route::any('/optimizeip/opipset', 'optimizeip/opipset');
@@ -99,7 +98,6 @@ Route::group(function () {
Route::post('/cert/order/data', 'cert/order_data');
Route::post('/cert/order/process', 'cert/order_process');
Route::post('/cert/order/:action', 'cert/order_op');
Route::get('/cert/order/import', 'cert/order_import');
Route::get('/cert/order/:action', 'cert/order_form');
Route::get('/cert/deploytask', 'cert/deploytask');
@@ -112,10 +110,11 @@ Route::group(function () {
Route::post('/cert/cname/data', 'cert/cname_data');
Route::post('/cert/cname/:action', 'cert/cname_op');
Route::any('/cert/certset', 'cert/certset');
Route::get('/cert/certset', 'cert/certset');
Route::any('/system/noticeset', 'system/noticeset');
Route::any('/system/proxyset', 'system/proxyset');
Route::get('/system/noticeset', 'system/noticeset');
Route::get('/system/proxyset', 'system/proxyset');
Route::post('/system/set', 'system/set');
Route::get('/system/mailtest', 'system/mailtest');
Route::get('/system/tgbottest', 'system/tgbottest');
Route::get('/system/webhooktest', 'system/webhooktest');
@@ -136,6 +135,8 @@ Route::group('api', function () {
Route::post('/record/remark/:id', 'domain/record_remark');
Route::post('/record/batch/:id', 'domain/record_batch');
Route::post('/cert/order', 'cert/order_info');
})->middleware(AuthApi::class);
Route::miss(function() {