From 236610d8fbdf678d1ae0ce46fafd6ddd9c57fa66 Mon Sep 17 00:00:00 2001 From: net909 Date: Sat, 31 May 2025 21:34:43 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=81=E4=B9=A6=E8=AE=A2=E5=8D=95=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=AE=BE=E7=BD=AE=E6=89=8B=E5=8A=A8=E7=BB=AD=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controller/Cert.php | 427 +++++++++++++++++--------------- app/lib/DeployHelper.php | 9 +- app/lib/deploy/btpanel.php | 30 ++- app/service/CertTaskService.php | 6 +- app/utils/MsgNotice.php | 31 ++- app/view/cert/certorder.html | 20 +- app/view/cert/order_form.html | 46 +++- app/view/cert/order_import.html | 167 ------------- route/app.php | 1 - 9 files changed, 343 insertions(+), 394 deletions(-) delete mode 100644 app/view/cert/order_import.html diff --git a/app/controller/Cert.php b/app/controller/Cert.php index 8d2c4a5..bea1528 100644 --- a/app/controller/Cert.php +++ b/app/controller/Cert.php @@ -66,7 +66,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 +80,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 +96,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 +108,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 +139,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 +156,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 +190,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); @@ -207,10 +207,10 @@ class Cert extends BaseController $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); } @@ -233,11 +233,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'])) { + $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 +256,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 +272,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 +345,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 +401,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 +419,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 +483,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 +567,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 +591,15 @@ 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']]; + $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 +611,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); @@ -622,8 +657,8 @@ class Cert extends BaseController $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'])); + $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 +687,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,18 +708,18 @@ 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; @@ -721,14 +756,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 +782,7 @@ 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']]; + $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'] . ')'; } @@ -756,9 +791,9 @@ class Cert extends BaseController $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'].')']; + $domains = Db::name('cert_domain')->where('oid', $row['id'])->order('sort', 'ASC')->column('domain'); + $domainstr = count($domains) > 2 ? implode('、', array_slice($domains, 0, 2)) . '等' . count($domains) . '个域名' : implode('、', $domains); + $orders[$row['id']] = ['name' => $row['id'] . '_' . $domainstr . '(' . CertHelper::$cert_config[$row['type']]['name'] . ')']; } View::assign('orders', $orders); @@ -829,7 +864,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 +875,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 +902,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]); diff --git a/app/lib/DeployHelper.php b/app/lib/DeployHelper.php index 3cabb64..20698df 100644 --- a/app/lib/DeployHelper.php +++ b/app/lib/DeployHelper.php @@ -41,9 +41,10 @@ class DeployHelper 'name' => '部署类型', 'type' => 'radio', 'options' => [ - '0' => '宝塔面板站点的证书', - '1' => '宝塔面板本身的证书', - '2' => '宝塔邮局域名的证书', + '0' => '网站的证书', + '3' => 'Docker网站的证书', + '2' => '邮局域名的证书', + '1' => '面板本身的证书', ], 'value' => '0', 'required' => true, @@ -53,7 +54,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, ], ], diff --git a/app/lib/deploy/btpanel.php b/app/lib/deploy/btpanel.php index db790b0..94b2686 100644 --- a/app/lib/deploy/btpanel.php +++ b/app/lib/deploy/btpanel.php @@ -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; diff --git a/app/service/CertTaskService.php b/app/service/CertTaskService.php index 77aeb6e..2d96015 100644 --- a/app/service/CertTaskService.php +++ b/app/service/CertTaskService.php @@ -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 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 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) { diff --git a/app/utils/MsgNotice.php b/app/utils/MsgNotice.php index 4820924..937eb35 100644 --- a/app/utils/MsgNotice.php +++ b/app/utils/MsgNotice.php @@ -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证书已签发成功!
证书账户: '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')
证书域名: '.implode('、', $domainList).'
签发时间: '.$row['issuetime'].'
到期时间: '.$row['expiretime'].'
颁发机构: '.$row['issuer']; + $mail_content = '尊敬的用户,您好:您有一张SSL证书将在'.config_get('cert_renewdays', 7).'天后到期,该证书为手动续期证书,请及时续期!
证书域名: '.implode('、', $domainList).'
签发时间: '.$row['issuetime'].'
到期时间: '.$row['expiretime'].'
颁发机构: '.$row['issuer']; } else { - $status_arr = [0 => '失败', -1 => '购买证书失败', -2 => '创建订单失败', -3 => '添加DNS失败', -4 => '验证DNS失败', -5 => '验证订单失败', -6 => '订单验证未通过', -7 => '签发证书失败']; - if(count($domainList) > 1){ - $mail_title = $domainList[0].'等'.count($domainList).'个域名SSL证书'.$status_arr[$row['status']].'通知'; - }else{ - $mail_title = $domainList[0].'域名SSL证书'.$status_arr[$row['status']].'通知'; + $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证书已签发成功!
证书账户: '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')
证书域名: '.implode('、', $domainList).'
签发时间: '.$row['issuetime'].'
到期时间: '.$row['expiretime'].'
颁发机构: '.$row['issuer']; + } else { + $status_arr = [0 => '失败', -1 => '购买证书失败', -2 => '创建订单失败', -3 => '添加DNS失败', -4 => '验证DNS失败', -5 => '验证订单失败', -6 => '订单验证未通过', -7 => '签发证书失败']; + if(count($domainList) > 1){ + $mail_title = $domainList[0].'等'.count($domainList).'个域名SSL证书'.$status_arr[$row['status']].'通知'; + }else{ + $mail_title = $domainList[0].'域名SSL证书'.$status_arr[$row['status']].'通知'; + } + $mail_content = '尊敬的用户,您好:您的SSL证书'.$status_arr[$row['status']].'!
证书账户: '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')
证书域名: '.implode('、', $domainList).'
失败时间: '.date('Y-m-d H:i:s').'
失败原因: '.$row['error'].''; } - $mail_content = '尊敬的用户,您好:您的SSL证书'.$status_arr[$row['status']].'!
证书账户: '.CertHelper::$cert_config[$type]['name'].'('.$row['aid'].')
证书域名: '.implode('、', $domainList).'
失败时间: '.date('Y-m-d H:i:s').'
失败原因: '.$row['error'].''; } $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s').''; diff --git a/app/view/cert/certorder.html b/app/view/cert/certorder.html index c1bcb75..6c11ea8 100644 --- a/app/view/cert/certorder.html +++ b/app/view/cert/certorder.html @@ -34,12 +34,6 @@ pre.pre-log{height: 330px;overflow-y: auto;width: 100%;background-color: rgba(51 刷新
@@ -87,7 +81,10 @@ $(document).ready(function(){ field: 'typename', title: '证书账户', formatter: function(value, row, index) { - return ''+value+'('+row.aid+')'; + if(value){ + return ''+value+'('+row.aid+')'; + } + return '手动续期'; } }, { @@ -208,7 +205,10 @@ $(document).ready(function(){ }else if(row.status == 2) { html += ' 继续验证  '; }else if(row.status == 3) { - html += ' 下载   续签  '; + html += ' 下载  '; + if(row.aid > 0){ + html += ' 续签  '; + } }else if(row.status == 4) { html += ' 重新申请  '; }else{ @@ -219,7 +219,9 @@ $(document).ready(function(){ html += '
  • 查看日志
  • '; if(row.status == 3){ html += '
  • 部署任务
  • '; - html += '
  • 吊销证书
  • '; + if(row.aid > 0){ + html += '
  • 吊销证书
  • '; + } }else if(row.status < 0){ html += '
  • 重置订单
  • '; }else if(row.status == 1 || row.status == 2){ diff --git a/app/view/cert/order_form.html b/app/view/cert/order_form.html index b8e9102..d03c1ee 100644 --- a/app/view/cert/order_form.html +++ b/app/view/cert/order_form.html @@ -22,9 +22,28 @@ {foreach $accounts as $k=>$v} {/foreach} +
    -
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    -
    +
    -
    +
    @@ -51,7 +70,8 @@
    -

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

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

    +

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

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

    +

    提示:选择手动续期,到达设置的续期天数,只会发送消息通知。

    @@ -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(); } }, }); diff --git a/app/view/cert/order_import.html b/app/view/cert/order_import.html deleted file mode 100644 index 7162be5..0000000 --- a/app/view/cert/order_import.html +++ /dev/null @@ -1,167 +0,0 @@ -{extend name="common/layout" /} -{block name="title"}导入已有证书{/block} -{block name="main"} - -
    -
    -
    -

    返回导入已有证书

    -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -{/block} -{block name="script"} - - - - -{/block} \ No newline at end of file diff --git a/route/app.php b/route/app.php index 7c69dcf..fbf75b2 100644 --- a/route/app.php +++ b/route/app.php @@ -98,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');