From 12d8017df5ff273e800271bea3e001d98b439048 Mon Sep 17 00:00:00 2001 From: net909 Date: Fri, 28 Feb 2025 23:04:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BE=A4=E8=BE=89=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E9=83=A8=E7=BD=B2=EF=BC=8C=E6=94=AF=E6=8C=81=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controller/Cert.php | 16 +++ app/lib/DeployHelper.php | 123 ++++++++++++++++++++- app/lib/deploy/kangleadmin.php | 173 ++++++++++++++++++++++++++++++ app/lib/deploy/synology.php | 147 +++++++++++++++++++++++++ app/view/cert/certorder.html | 13 +++ app/view/cert/deploytask.html | 3 + public/static/images/ctyun.ico | Bin 0 -> 67646 bytes public/static/images/synology.png | Bin 0 -> 1072 bytes 8 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 app/lib/deploy/kangleadmin.php create mode 100644 app/lib/deploy/synology.php create mode 100644 public/static/images/ctyun.ico create mode 100644 public/static/images/synology.png diff --git a/app/controller/Cert.php b/app/controller/Cert.php index 5e039e8..c77269c 100644 --- a/app/controller/Cert.php +++ b/app/controller/Cert.php @@ -203,6 +203,7 @@ class Cert extends BaseController $domain = $this->request->post('domain', null, 'trim'); $id = input('post.id'); $type = input('post.type', null, 'trim'); + $status = input('post.status', null, 'trim'); $offset = input('post.offset/d'); $limit = input('post.limit/d'); @@ -216,6 +217,17 @@ class Cert extends BaseController if (!empty($type)) { $select->where('B.type', $type); } + if (!isNullOrEmpty($status)) { + if ($status == '5') { + $select->where('A.status', '<', 0); + } elseif ($status == '6') { + $select->where('A.expiretime', '<', date('Y-m-d H:i:s', time() + 86400 * 7))->where('A.expiretime', '>=', date('Y-m-d H:i:s')); + } elseif ($status == '7') { + $select->where('A.expiretime', '<', date('Y-m-d H:i:s')); + } else { + $select->where('A.status', $status); + } + } $total = $select->count(); $rows = $select->fieldRaw('A.*,B.type,B.remark aremark')->order('id', 'desc')->limit($offset, $limit)->select(); @@ -541,6 +553,7 @@ class Cert extends BaseController $domain = $this->request->post('domain', null, 'trim'); $oid = input('post.oid'); $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'); @@ -555,6 +568,9 @@ class Cert extends BaseController if (!empty($type)) { $select->where('B.type', $type); } + if (!isNullOrEmpty($status)) { + $select->where('A.status', $status); + } if (!empty($remark)) { $select->where('A.remark', $remark); } diff --git a/app/lib/DeployHelper.php b/app/lib/DeployHelper.php index 409dc1e..5de81c1 100644 --- a/app/lib/DeployHelper.php +++ b/app/lib/DeployHelper.php @@ -59,7 +59,7 @@ class DeployHelper ], ], 'kangle' => [ - 'name' => 'Kangle', + 'name' => 'Kangle用户', 'class' => 1, 'icon' => 'host.png', 'note' => '以上登录信息为Easypanel用户面板的,非管理员面板。如选网站密码认证类型,则用户面板登录不能开启验证码。', @@ -131,6 +131,72 @@ class DeployHelper ], ], ], + 'kangleadmin' => [ + 'name' => 'Kangle管理员', + 'class' => 1, + 'icon' => 'host.png', + 'note' => '以上登录地址需填写Easypanel管理员面板地址,非用户面板。', + 'inputs' => [ + 'url' => [ + 'name' => '面板地址', + 'type' => 'input', + 'placeholder' => 'Easypanel管理员面板地址', + 'note' => '填写规则如:http://192.168.1.100:3312 ,不要带其他后缀', + 'required' => true, + ], + 'path' => [ + 'name' => '管理员面板路径', + 'type' => 'input', + 'placeholder' => '留空默认为/admin', + ], + 'username' => [ + 'name' => '管理员用户名', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'skey' => [ + 'name' => '面板安全码', + 'type' => 'input', + 'placeholder' => '管理员面板->服务器设置->面板通信安全码', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'name' => [ + 'name' => '网站用户名', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'type' => [ + 'name' => '部署类型', + 'type' => 'radio', + 'options' => [ + '0' => '网站SSL证书', + '1' => '单域名SSL证书(仅CDN支持)', + ], + 'value' => '0', + 'required' => true, + ], + 'domains' => [ + 'name' => 'CDN域名列表', + 'type' => 'textarea', + 'placeholder' => '填写要部署证书的域名,每行一个', + 'show' => 'type==1', + 'required' => true, + ], + ], + ], 'safeline' => [ 'name' => '雷池WAF', 'class' => 1, @@ -400,6 +466,61 @@ class DeployHelper ], ], ], + 'synology' => [ + 'name' => '群辉面板', + 'class' => 1, + 'icon' => 'synology.png', + 'note' => null, + 'tasknote' => '', + 'inputs' => [ + 'url' => [ + 'name' => '面板地址', + 'type' => 'input', + 'placeholder' => '群辉面板地址', + 'note' => '填写规则如:http://192.168.1.100:5000 ,不要带其他后缀', + 'required' => true, + ], + 'username' => [ + 'name' => '登录账号', + 'type' => 'input', + 'placeholder' => '必须是处于管理员用户组,不能开启双重认证', + 'required' => true, + ], + 'password' => [ + 'name' => '登录密码', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'version' => [ + 'name' => '群辉版本', + 'type' => 'radio', + 'options' => [ + '0' => '7.x', + '1' => '6.x', + ], + 'value' => '0', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'desc' => [ + 'name' => '群晖证书描述', + 'type' => 'input', + 'placeholder' => '', + 'note' => '根据证书描述匹配替换对应证书,留空则根据证书通用名匹配', + ], + ], + ], 'aliyun' => [ 'name' => '阿里云', 'class' => 2, diff --git a/app/lib/deploy/kangleadmin.php b/app/lib/deploy/kangleadmin.php new file mode 100644 index 0000000..35d223b --- /dev/null +++ b/app/lib/deploy/kangleadmin.php @@ -0,0 +1,173 @@ +url = rtrim($config['url'], '/'); + if (empty($config['path'])) $config['path'] = '/admin'; + $this->path = rtrim($config['path'], '/'); + $this->username = $config['username']; + $this->skey = $config['skey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->username) || empty($this->skey)) throw new Exception('必填参数不能为空'); + $this->login(); + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['name'])) throw new Exception('网站用户名不能为空'); + $this->login(); + $this->log('登录成功 cookie:' . $this->cookie); + $this->loginVhost($config['name']); + + if ($config['type'] == '1' && !empty($config['domains'])) { + $domains = explode("\n", $config['domains']); + $success = 0; + $errmsg = null; + foreach ($domains as $domain) { + $domain = trim($domain); + if (empty($domain)) continue; + try { + $this->deployDomain($domain, $fullchain, $privatekey); + $this->log("域名 {$domain} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("域名 {$domain} 证书部署失败:" . $errmsg); + } + } + if ($success == 0) { + throw new Exception($errmsg ? $errmsg : '要部署的域名不存在'); + } + } else { + $this->deployAccount($fullchain, $privatekey); + $this->log("账号级SSL证书部署成功"); + } + } + + private function deployDomain($domain, $fullchain, $privatekey) + { + $path = '/vhost/?c=ssl&a=domainSsl'; + $post = [ + 'domain' => $domain, + 'certificate' => $fullchain, + 'certificate_key' => $privatekey, + ]; + $response = curl_client($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); + if (strpos($response['body'], '成功')) { + return true; + } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { + throw new Exception(htmlspecialchars($match[1])); + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception(htmlspecialchars($response['body'])); + } else { + throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); + } + } + + private function deployAccount($fullchain, $privatekey) + { + $path = '/vhost/?c=ssl&a=ssl'; + $post = [ + 'certificate' => $fullchain, + 'certificate_key' => $privatekey, + ]; + $response = curl_client($this->url . $path, http_build_query($post), null, $this->cookie, null, $this->proxy); + if (strpos($response['body'], '成功')) { + return true; + } elseif (preg_match('/alert\(\'(.*?)\'\)/i', $response['body'], $match)) { + throw new Exception(htmlspecialchars($match[1])); + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception(htmlspecialchars($response['body'])); + } else { + throw new Exception('原因未知(httpCode=' . $response['code'] . ')'); + } + } + + private function login() + { + $url = $this->url . $this->path . '/index.php?c=sso&a=hello&url=' . urlencode($this->url . $this->path . '/index.php?'); + $response = curl_client($url, null, null, null, null, $this->proxy); + if ($response['code'] == 302 && !empty($response['redirect_url'])) { + $cookie = ''; + if (preg_match_all('/Set-Cookie: (.*);/iU', $response['header'], $matchs)) { + foreach ($matchs[1] as $val) { + $arr = explode('=', $val); + if ($arr[1] == '' || $arr[1] == 'deleted') continue; + $cookie .= $val . '; '; + } + $query = parse_url($response['redirect_url'], PHP_URL_QUERY); + parse_str($query, $params); + if (isset($params['r'])) { + $sess_key = $params['r']; + $this->login2($cookie, $sess_key); + $this->cookie = $cookie; + return true; + } else { + throw new Exception('获取SSO凭据失败,sess_key获取失败'); + } + } else { + throw new Exception('获取SSO凭据失败,获取cookie失败'); + } + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception('获取SSO凭据失败 (' . htmlspecialchars($response['body']) . ')'); + } else { + throw new Exception('获取SSO凭据失败 (httpCode=' . $response['code'] . ')'); + } + } + + private function login2($cookie, $sess_key) + { + $s = md5($sess_key . $this->username . $sess_key . $this->skey); + $url = $this->url . $this->path . '/index.php?c=sso&a=login&name=' . $this->username . '&r=' . $sess_key . '&s=' . $s; + $response = curl_client($url, null, null, $cookie, null, $this->proxy); + if ($response['code'] == 302) { + return true; + } elseif (strlen($response['body']) > 3 && strlen($response['body']) < 50) { + throw new Exception('SSO登录失败 (' . htmlspecialchars($response['body']) . ')'); + } else { + throw new Exception('SSO登录失败 (httpCode=' . $response['code'] . ')'); + } + } + + private function loginVhost($name) + { + $url = $this->url . $this->path . '/index.php?c=vhost&a=impLogin&name=' . $name; + $response = curl_client($url, null, null, $this->cookie, null, $this->proxy); + if ($response['code'] == 302) { + curl_client($this->url . '/vhost/', null, null, $this->cookie, null, $this->proxy); + } else { + throw new Exception('用户面板登录失败 (httpCode=' . $response['code'] . ')'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/lib/deploy/synology.php b/app/lib/deploy/synology.php new file mode 100644 index 0000000..ae7fd39 --- /dev/null +++ b/app/lib/deploy/synology.php @@ -0,0 +1,147 @@ +url = rtrim($config['url'], '/'); + $this->username = $config['username']; + $this->password = $config['password']; + $this->version = $config['version']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->username) || empty($this->password)) throw new Exception('必填内容不能为空'); + $this->login(); + } + + private function login() + { + $url = $this->url . '/webapi/' . ($this->version == '1' ? 'auth.cgi' : 'entry.cgi'); + $params = [ + 'api' => 'SYNO.API.Auth', + 'version' => 6, + 'method' => 'login', + 'account' => $this->username, + 'passwd' => $this->password, + 'format' => 'sid', + 'enable_syno_token' => 'yes', + ]; + $response = curl_client($url, http_build_query($params), null, null, null, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['success']) && $result['success']) { + $this->token = $result['data']; + } elseif(isset($result['error'])) { + throw new Exception('登录失败:' . $result['error']); + } else { + throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); + } + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + + $url = $this->url . '/webapi/entry.cgi'; + $params = [ + 'api' => 'SYNO.Core.Certificate.CRT', + 'version' => 1, + 'method' => 'list', + '_sid' => $this->token['sid'], + 'SynoToken' => $this->token['synotoken'], + ]; + $response = curl_client($url, http_build_query($params), null, null, null, $this->proxy); + $result = json_decode($response['body'], true); + if (isset($result['success']) && $result['success']) { + $this->log('获取证书列表成功'); + } elseif(isset($result['error'])) { + throw new Exception('获取证书列表失败:' . $result['error']); + } else { + throw new Exception('获取证书列表失败(httpCode=' . $response['code'] . ')'); + } + + $id = null; + foreach ($result['data']['certificates'] as $certificate) { + if ($certificate['subject']['common_name'] == $certInfo['subject']['CN'] || $certificate['desc'] == $config['desc']) { + $id = $certificate['id']; + break; + } + } + if ($id) { + $this->import($fullchain, $privatekey, $config, $id); + } else { + $this->import($fullchain, $privatekey, $config); + } + } + + private function import($fullchain, $privatekey, $config, $id = null) + { + $url = $this->url . '/webapi/entry.cgi'; + $params = [ + 'api' => 'SYNO.Core.Certificate', + 'version' => 1, + 'method' => 'import', + '_sid' => $this->token['sid'], + 'SynoToken' => $this->token['synotoken'], + ]; + $privatekey_file = tempnam(sys_get_temp_dir(), 'privatekey'); + file_put_contents($privatekey_file, $privatekey); + $fullchain_file = tempnam(sys_get_temp_dir(), 'fullchain'); + file_put_contents($fullchain_file, $fullchain); + $post = [ + 'key' => new \CURLFile($privatekey_file), + 'cert' => new \CURLFile($fullchain_file), + 'id' => $id, + 'desc' => $config['desc'], + ]; + $response = curl_client($url . '?' . http_build_query($params), $post, null, null, null, $this->proxy); + unlink($privatekey_file); + unlink($fullchain_file); + $result = json_decode($response['body'], true); + if ($id) { + if (isset($result['success']) && $result['success']) { + $this->log('证书ID:'.$id.'更新成功!'); + } elseif(isset($result['error'])) { + throw new Exception('证书ID:'.$id.'更新失败:' . $result['error']); + } else { + throw new Exception('证书ID:'.$id.'更新失败(httpCode=' . $response['code'] . ')'); + } + } else { + if (isset($result['success']) && $result['success']) { + $this->log('证书上传成功!'); + } elseif(isset($result['error'])) { + throw new Exception('证书上传失败:' . $result['error']); + } else { + throw new Exception('证书上传失败(httpCode=' . $response['code'] . ')'); + } + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/view/cert/certorder.html b/app/view/cert/certorder.html index 44251e7..9e7bfde 100644 --- a/app/view/cert/certorder.html +++ b/app/view/cert/certorder.html @@ -27,6 +27,9 @@ pre.pre-log{height: 330px;overflow-y: auto;width: 100%;background-color: rgba(51 {/foreach} +
+ +
刷新
@@ -137,6 +140,16 @@ $(document).ready(function(){ } else if(value == 3) { return '已签发'; } else if(value == 2) { + if(row.retrytime != null){ + var now = new Date().getTime(); + var retry = new Date(row.retrytime).getTime(); + var diff = retry - now; + if(diff > 0){ + var min = Math.floor(diff / 60000); + var sec = Math.floor((diff - min * 60000) / 1000); + return '正在验证'; + } + } return '正在验证'; } else if(value == 1) { if(row.retrytime != null){ diff --git a/app/view/cert/deploytask.html b/app/view/cert/deploytask.html index fe07dca..1290873 100644 --- a/app/view/cert/deploytask.html +++ b/app/view/cert/deploytask.html @@ -29,6 +29,9 @@ pre.pre-log{height: 330px;overflow-y: auto;width: 100%;background-color: rgba(51 {/foreach}
+
+ +
刷新 添加 diff --git a/public/static/images/ctyun.ico b/public/static/images/ctyun.ico new file mode 100644 index 0000000000000000000000000000000000000000..0c1956075724c4abc808fc589f432cf7448ab157 GIT binary patch literal 67646 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 Er+eYh=l}o! literal 0 HcmV?d00001 diff --git a/public/static/images/synology.png b/public/static/images/synology.png new file mode 100644 index 0000000000000000000000000000000000000000..9f33d3bcb00aba799026ca244260b72adfab4ec8 GIT binary patch literal 1072 zcmV-01kd}4P)r%oYIr;-(>>{u4MSAM^(Y2i3W_`X*i@C@d^=98BQp>8Y1Z&tQCf+ySbc zhldBMtgO_sO$N~k%*fD(hlf=zlO#njG&JM@o0^(5;L6I1V>5y<>^cp6czEc5)y~4g zf`&`_@rJP!DH^4aLfOpv%z#Hqj!CVF{!p_`i-}K=87b3Pft&#%}wz7`l^AjiKMKruWL4K4SIWfCC;-Ahr`|kn0tGByVd#a z?Tw0yi%sX>CYfaG7wI!HhjgIL;Oq#D1nFD^D8o`jv9o#b^+ z+n)IO`8oCU^aPoRxP^EE>+0(0{r%k*gRra4&d$Vv>4kw~fCI&Q1F7dA$c~PVyiR5@ ziSF)hz+6&ZUhX+b>_V#%1&4|@Wol}Qa9{KC!!c4-RYj$xrBqZ@B<_`#;#K|a2_QgY zW23m3nMtTR9z`q`qv`2sYHe+$tgI}RJV@U=0XfV5{(ic>y;b9W*vFN7baeFpBp?%! zEspqsfdN`vT%`T|ecxp8lCG_-^-ILQ0-2eagw4~?&_F0d9Mm!auNFQtGc&G)4h|0d zeC-LSmpREeIXUS^By82y)e`QsmzS4*e)a@%a&knCJ1of#Dg#U3+}tcyh<}VxO-&8q zX1cexXTpWv*Vp&=3CLw08ymBd1{uV;QBqRk7o6}q#P#kQ|M?0&dw7{Ker;s+_4TZx zqJrh;=QI9!#_sR$+1=e8+uhw|8yg#pf3vcalM`RQr12M8B=MDRLb)P{DK5uI0s;kZ qC#=YaFDQ