11 Commits
2.7.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
59 changed files with 706 additions and 697 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;
}
@@ -462,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;
}
@@ -496,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,11 +246,15 @@ 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;
}
@@ -252,7 +269,7 @@ class Cert extends BaseController
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')]]);
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()
@@ -268,32 +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,
'isauto' => 1,
];
$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),
@@ -307,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),
@@ -341,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();
@@ -425,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;
@@ -489,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', '无权限');
@@ -518,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()]);
}
}
@@ -542,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'] . '';
}
@@ -561,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);
@@ -592,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);
}
@@ -619,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;
}
@@ -652,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'),
@@ -673,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();
@@ -703,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 . '个任务']);
@@ -721,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()]);
}
}
@@ -747,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'] . '';
}
@@ -755,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);
@@ -786,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 . '%');
}
@@ -829,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记录值']);
@@ -840,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'),
@@ -867,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]);
@@ -883,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

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

@@ -177,7 +177,7 @@ location / {
'tencent' => [
'name' => '腾讯云免费SSL',
'class' => 2,
'icon' => 'tencent.ico',
'icon' => 'tencent.png',
'wildcard' => false,
'max_domains' => 1,
'cname' => false,
@@ -215,7 +215,7 @@ location / {
'aliyun' => [
'name' => '阿里云免费SSL',
'class' => 2,
'icon' => 'aliyun.ico',
'icon' => 'aliyun.png',
'wildcard' => false,
'max_domains' => 1,
'cname' => false,

View File

@@ -10,7 +10,8 @@ class DeployHelper
'btpanel' => [
'name' => '宝塔面板',
'class' => 1,
'icon' => 'bt.ico',
'icon' => 'bt.png',
'desc' => '支持部署到宝塔面板搭建的站点、Docker、邮局与面板本身',
'note' => null,
'inputs' => [
'url' => [
@@ -41,9 +42,10 @@ class DeployHelper
'name' => '部署类型',
'type' => 'radio',
'options' => [
'0' => '宝塔面板站点的证书',
'1' => '宝塔面板本身的证书',
'2' => '宝塔邮局域名的证书',
'0' => '网站的证书',
'3' => 'Docker网站的证书',
'2' => '邮局域名的证书',
'1' => '面板本身的证书',
],
'value' => '0',
'required' => true,
@@ -53,7 +55,7 @@ class DeployHelper
'type' => 'textarea',
'placeholder' => '填写要部署证书的网站名称,每行一个',
'note' => 'PHP项目和反代项目填写创建时绑定的第一个域名Java/Node/Go等其他项目填写项目名称邮局填写域名',
'show' => 'type==0||type==2',
'show' => 'type==0||type==2||type==3',
'required' => true,
],
],
@@ -62,6 +64,7 @@ class DeployHelper
'name' => 'Kangle用户',
'class' => 1,
'icon' => 'host.png',
'desc' => '支持虚拟主机与CDN站点',
'note' => '以上登录信息为Easypanel用户面板的非管理员面板。如选网站密码认证类型则用户面板登录不能开启验证码。',
'inputs' => [
'url' => [
@@ -135,6 +138,7 @@ class DeployHelper
'name' => 'Kangle管理员',
'class' => 1,
'icon' => 'host.png',
'desc' => '支持虚拟主机与CDN站点',
'note' => '以上登录地址需填写Easypanel管理员面板地址非用户面板。',
'inputs' => [
'url' => [
@@ -201,6 +205,7 @@ class DeployHelper
'name' => '雷池WAF',
'class' => 1,
'icon' => 'safeline.png',
'desc' => '',
'note' => null,
'tasknote' => '系统会根据关联SSL证书的域名自动更新对应证书',
'inputs' => [
@@ -232,7 +237,8 @@ class DeployHelper
'btwaf' => [
'name' => '堡塔云WAF',
'class' => 1,
'icon' => 'bt.ico',
'icon' => 'bt.png',
'desc' => '',
'note' => null,
'tasknote' => '',
'inputs' => [
@@ -272,6 +278,7 @@ class DeployHelper
'name' => 'Cdnfly',
'class' => 1,
'icon' => 'waf.png',
'desc' => '',
'note' => '登录Cdnfly控制台->账户中心->API密钥点击开启后获取',
'inputs' => [
'url' => [
@@ -317,6 +324,7 @@ class DeployHelper
'name' => 'LeCDN',
'class' => 1,
'icon' => 'waf.png',
'desc' => '',
'note' => null,
'inputs' => [
'url' => [
@@ -362,6 +370,7 @@ class DeployHelper
'name' => 'GoEdge',
'class' => 1,
'icon' => 'waf.png',
'desc' => '支持GoEdge与FlexCDN',
'note' => '需要先<a href="https://goedge.cloud/docs/API/Settings.md" target="_blank" rel="noreferrer">开启HTTP API端口</a>',
'tasknote' => '系统会根据关联SSL证书的域名自动更新对应证书',
'inputs' => [
@@ -420,6 +429,7 @@ class DeployHelper
'name' => '1Panel',
'class' => 1,
'icon' => 'opanel.png',
'desc' => '更新面板证书管理内的SSL证书',
'note' => null,
'tasknote' => '系统会根据关联SSL证书的域名自动更新对应证书',
'inputs' => [
@@ -462,6 +472,7 @@ class DeployHelper
'name' => 'MW面板',
'class' => 1,
'icon' => 'mwpanel.ico',
'desc' => '',
'note' => null,
'tasknote' => '',
'inputs' => [
@@ -519,6 +530,7 @@ class DeployHelper
'name' => '耗子面板',
'class' => 1,
'icon' => 'ratpanel.ico',
'desc' => '支持耗子面板 v2.5+ 版本使用',
'note' => '支持耗子面板 v2.5+ 版本使用',
'inputs' => [
'url' => [
@@ -577,6 +589,7 @@ class DeployHelper
'name' => '群晖面板',
'class' => 1,
'icon' => 'synology.png',
'desc' => '支持群晖DSM 6.x/7.x版本',
'note' => null,
'tasknote' => '',
'inputs' => [
@@ -632,6 +645,7 @@ class DeployHelper
'name' => 'Proxmox VE',
'class' => 1,
'icon' => 'proxmox.ico',
'desc' => '部署到PVE节点',
'note' => '在“权限->API令牌”添加令牌不要选特权分离',
'tasknote' => '',
'inputs' => [
@@ -676,8 +690,9 @@ class DeployHelper
'aliyun' => [
'name' => '阿里云',
'class' => 2,
'icon' => 'aliyun.ico',
'note' => '支持部署到阿里云CDN、ESA、SLB、OSS、WAF等服务',
'icon' => 'aliyun.png',
'desc' => '支持部署到阿里云CDN、ESA、SLB、OSS、WAF、FC等服务',
'note' => '支持部署到阿里云CDN、ESA、SLB、OSS、WAF、FC等服务',
'tasknote' => '',
'inputs' => [
'AccessKeyId' => [
@@ -823,7 +838,8 @@ class DeployHelper
'tencent' => [
'name' => '腾讯云',
'class' => 2,
'icon' => 'tencent.ico',
'icon' => 'tencent.png',
'desc' => '支持部署到腾讯云CDN、EO、CLB、COS、TKE、SCF等服务',
'note' => '支持部署到腾讯云CDN、EO、CLB、COS、TKE、SCF等服务',
'tasknote' => '',
'inputs' => [
@@ -958,6 +974,7 @@ class DeployHelper
'name' => '华为云',
'class' => 2,
'icon' => 'huawei.ico',
'desc' => '支持部署到华为云CDN、ELB、WAF等服务',
'note' => '支持部署到华为云CDN、ELB、WAF等服务',
'inputs' => [
'AccessKeyId' => [
@@ -1030,6 +1047,7 @@ class DeployHelper
'name' => 'UCloud',
'class' => 2,
'icon' => 'ucloud.ico',
'desc' => '支持部署到UCDN',
'note' => '支持部署到UCDN',
'inputs' => [
'PublicKey' => [
@@ -1059,6 +1077,7 @@ class DeployHelper
'name' => '七牛云',
'class' => 2,
'icon' => 'qiniu.ico',
'desc' => '支持部署到七牛云CDN、OSS',
'note' => '支持部署到七牛云CDN、OSS',
'inputs' => [
'AccessKey' => [
@@ -1114,6 +1133,7 @@ class DeployHelper
'name' => '多吉云',
'class' => 2,
'icon' => 'cloud.png',
'desc' => '支持部署到多吉云融合CDN',
'note' => '支持部署到多吉云融合CDN',
'inputs' => [
'AccessKey' => [
@@ -1151,6 +1171,8 @@ class DeployHelper
'name' => '又拍云',
'class' => 2,
'icon' => 'upyun.ico',
'desc' => '支持部署到又拍云CDN',
'note' => '支持部署到又拍云CDN',
'tasknote' => '系统会根据关联SSL证书的域名进行证书的迁移操作',
'inputs' => [
'username' => [
@@ -1180,6 +1202,7 @@ class DeployHelper
'name' => '百度云',
'class' => 2,
'icon' => 'baidu.ico',
'desc' => '支持部署到百度云CDN',
'note' => '支持部署到百度云CDN',
'inputs' => [
'AccessKeyId' => [
@@ -1217,6 +1240,7 @@ class DeployHelper
'name' => '火山引擎',
'class' => 2,
'icon' => 'huoshan.ico',
'desc' => '支持部署到火山引擎CDN',
'note' => '支持部署到火山引擎CDN',
'inputs' => [
'AccessKeyId' => [
@@ -1284,6 +1308,7 @@ class DeployHelper
'name' => '西部数码',
'class' => 2,
'icon' => 'west.ico',
'desc' => '支持部署到西部数码虚拟主机',
'note' => '支持部署到西部数码虚拟主机',
'inputs' => [
'username' => [
@@ -1321,6 +1346,7 @@ class DeployHelper
'name' => '网宿科技',
'class' => 2,
'icon' => 'wangsu.ico',
'desc' => '支持部署到网宿CDN',
'note' => '适用产品网页加速、下载分发、全站加速、点播分发、直播分发、上传加速、移动加速、上网加速、S-P2P、PCDN、应用性能管理、WEB应用防火墙、BotGuard爬虫管理、WSS、DMS、DDoS云清洗、应用加速、应用安全加速解决方案、IPv6一体化解决方案、电商安全加速解决方案、金融安全加速解决方案、政企安全加速解决方案、DDoS云清洗(非网站业务)、区块链安全加速解决方案、IPv6安全加速解决方案、CDN Pro。暂不支持AKSK鉴权。',
'inputs' => [
'username' => [
@@ -1389,6 +1415,7 @@ class DeployHelper
'name' => '白山云',
'class' => 2,
'icon' => 'waf.png',
'desc' => '替换白山云证书管理内的证书',
'note' => null,
'inputs' => [
'account' => [
@@ -1428,7 +1455,8 @@ class DeployHelper
'name' => '天翼云',
'class' => 2,
'icon' => 'ctyun.ico',
'note' => '支持部署到天翼云CDN',
'desc' => '支持部署到天翼云CDN、边缘加速',
'note' => '支持部署到天翼云CDN、边缘加速',
'inputs' => [
'AccessKeyId' => [
'name' => 'AccessKeyId',
@@ -1476,6 +1504,7 @@ class DeployHelper
'name' => '括彩云',
'class' => 2,
'icon' => 'kuocai.jpg',
'desc' => '替换括彩云证书管理内的证书',
'note' => '支持括彩云及其代理商,填写控制台登录账号及密码',
'inputs' => [
'username' => [
@@ -1511,41 +1540,11 @@ class DeployHelper
],
],
],
'allwaf' => [
'name' => 'AllWAF',
'class' => 2,
'icon' => 'waf.png',
'note' => '在<a href="https://user.allwaf.cn/" target="_blank" rel="noreferrer">ALLWAF</a>访问控制页面创建AccessKey',
'tasknote' => '系统会根据关联SSL证书的域名自动更新对应证书',
'inputs' => [
'accessKeyId' => [
'name' => 'AccessKey ID',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'accessKey' => [
'name' => 'AccessKey密钥',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'taskinputs' => [],
],
'aws' => [
'name' => 'AWS',
'class' => 2,
'icon' => 'aws.ico',
'icon' => 'aws.png',
'desc' => '支持部署到Amazon CloudFront、AWS Certificate Manager',
'note' => '支持部署到Amazon CloudFront、AWS Certificate Manager',
'inputs' => [
'AccessKeyId' => [
@@ -1602,6 +1601,7 @@ class DeployHelper
'name' => 'Gcore',
'class' => 2,
'icon' => 'gcore.ico',
'desc' => '替换Gcore CDN证书',
'note' => '在 个人资料->API令牌 页面创建API令牌',
'inputs' => [
'account' => [
@@ -1646,6 +1646,7 @@ class DeployHelper
'name' => 'Cachefly',
'class' => 2,
'icon' => 'cloud.png',
'desc' => '替换Cachefly CDN证书',
'note' => '在 API Tokens 页面生成 API Token',
'inputs' => [
'account' => [
@@ -1678,6 +1679,7 @@ class DeployHelper
'name' => 'SSH服务器',
'class' => 3,
'icon' => 'server.png',
'desc' => '可通过SSH连接到Linux/Windows服务器并部署证书',
'note' => '可通过SSH连接到Linux/Windows服务器并部署证书php需要安装ssh2扩展',
'tasknote' => '请确保路径存在且有写入权限,路径一定要以/开头Windows路径请使用/代替\,且需要在最开头加/',
'inputs' => [
@@ -1804,6 +1806,7 @@ class DeployHelper
'name' => 'FTP服务器',
'class' => 3,
'icon' => 'server.png',
'desc' => '可将证书上传到FTP服务器',
'note' => '可将证书上传到FTP服务器php需要安装ftp扩展',
'tasknote' => '请确保路径存在且有写入权限',
'inputs' => [
@@ -1887,6 +1890,7 @@ class DeployHelper
'name' => '复制到本机',
'class' => 3,
'icon' => 'server2.png',
'desc' => '将证书复制到本机指定路径',
'note' => '将证书复制到本机指定路径',
'tasknote' => '请确保php进程有对证书保存路径的写入权限宝塔面板需关闭防跨站攻击如果当前是Docker运行的则需要做目录映射到宿主机。',
'inputs' => [],

View File

@@ -321,7 +321,7 @@ class ACMEv2
}
));
if ($this->proxy) {
if ($this->proxy == 1) {
curl_set_proxy($this->ch);
}

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

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

@@ -72,7 +72,7 @@ class google 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 "pki.goog"'];
}*/

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

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

@@ -60,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']];
}
}
@@ -97,6 +98,12 @@ 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'],

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

@@ -64,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"'];
}*/

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

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

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

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

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

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

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

@@ -78,7 +78,7 @@
<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="checkurl" v-model="set.checkurl" placeholder="留空默认为解析记录值IP" class="form-control" data-bv-uri="true" required>
<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">

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

@@ -271,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>';

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' => '1035',
'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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 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');