From 64b5221787f495300aa042b8c8f386df8c262720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E5=B1=B1=E5=A4=A7=E6=9F=A0=E6=AA=AC?= <34309188+Beelkic@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:23:06 +0800 Subject: [PATCH 1/4] =?UTF-8?q?1panel=E6=94=AF=E6=8C=81=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E5=AD=90=E8=8A=82=E7=82=B9=E9=83=A8=E7=BD=B2=20(#356)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/DeployHelper.php | 6 +- app/lib/deploy/opanel.php | 126 +++++++++++++++++++++++++++++++------- 2 files changed, 108 insertions(+), 24 deletions(-) diff --git a/app/lib/DeployHelper.php b/app/lib/DeployHelper.php index 0fb2a0e..2427ece 100644 --- a/app/lib/DeployHelper.php +++ b/app/lib/DeployHelper.php @@ -614,9 +614,9 @@ class DeployHelper ], 'node_name' => [ 'name' => '子节点名称', - 'type' => 'input', - 'placeholder' => '', - 'note' => '不填写时,将替换主控节点证书;否则,将替换被控节点证书', + 'type' => 'textarea', + 'placeholder' => '每行一个节点名称', + 'note' => '不填写时,将替换主控节点证书;否则,将替换被控节点证书。多个节点请每行填写一个', 'show' => 'type==0', ], ], diff --git a/app/lib/deploy/opanel.php b/app/lib/deploy/opanel.php index 5e2872b..2c008ce 100644 --- a/app/lib/deploy/opanel.php +++ b/app/lib/deploy/opanel.php @@ -11,7 +11,6 @@ class opanel implements DeployInterface private $url; private $key; private $proxy; - private $nodeName; public function __construct($config) { @@ -28,7 +27,11 @@ class opanel implements DeployInterface public function deploy($fullchain, $privatekey, $config, &$info) { + // 解析节点名称列表 + $nodeNames = $this->parseNodeNames($config); + if (isset($config['type']) && $config['type'] == '3') { + // 面板本身的证书部署 $params = [ 'cert' => $fullchain, 'key' => $privatekey, @@ -36,18 +39,66 @@ class opanel implements DeployInterface 'sslID' => null, 'sslType' => 'import-paste', ]; - try { - $this->request('/core/settings/ssl/update', $params); - $this->log("面板证书更新成功!"); + + if (empty($nodeNames)) { + // 没有指定节点,部署到主控节点 + try { + $this->request('/core/settings/ssl/update', $params); + $this->log("面板证书更新成功!"); + return; + } catch (Exception $e) { + throw new Exception("面板证书更新失败:" . $e->getMessage()); + } + } else { + // 部署到多个子节点 + $successCount = 0; + $failCount = 0; + foreach ($nodeNames as $nodeName) { + try { + $this->request('/core/settings/ssl/update', $params, $nodeName); + $this->log("节点 [{$nodeName}] 面板证书更新成功!"); + $successCount++; + } catch (Exception $e) { + $this->log("节点 [{$nodeName}] 面板证书更新失败:" . $e->getMessage()); + $failCount++; + } + } + if ($failCount > 0 && $successCount == 0) { + throw new Exception("所有节点证书更新失败"); + } return; - } catch (Exception $e) { - throw new Exception("面板证书更新失败:" . $e->getMessage()); } } - if (isset($config['node_name'])) $this->nodeName = $config['node_name']; + // 如果没有指定节点,则部署到主控节点 + if (empty($nodeNames)) { + $this->deployToNode($fullchain, $privatekey, $config, null); + } else { + // 部署到多个子节点 + $successCount = 0; + $failCount = 0; + foreach ($nodeNames as $nodeName) { + try { + $this->deployToNode($fullchain, $privatekey, $config, $nodeName); + $successCount++; + } catch (Exception $e) { + $this->log("节点 [{$nodeName}] 部署失败:" . $e->getMessage()); + $failCount++; + } + } + if ($failCount > 0 && $successCount == 0) { + throw new Exception("所有节点部署失败"); + } + } + } + /** + * 部署到指定节点 + */ + private function deployToNode($fullchain, $privatekey, $config, $nodeName = null) + { if (!empty($config['id'])) { + // 指定证书ID的情况 $params = [ 'sslID' => intval($config['id']), 'type' => 'paste', @@ -56,23 +107,28 @@ class opanel implements DeployInterface 'description' => '', ]; try { - $this->request('/websites/ssl/upload', $params); - $this->log("证书ID:{$config['id']}更新成功!"); + $this->request('/websites/ssl/upload', $params, $nodeName); + $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$config['id']}更新成功!" : "证书ID:{$config['id']}更新成功!"; + $this->log($logMsg); return; } catch (Exception $e) { - throw new Exception("证书ID:{$config['id']}更新失败:" . $e->getMessage()); + $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$config['id']}更新失败:" : "证书ID:{$config['id']}更新失败:"; + throw new Exception($logMsg . $e->getMessage()); } } + // 根据域名自动匹配证书 $domains = $config['domainList']; if (empty($domains)) throw new Exception('没有设置要部署的域名'); $params = ['page' => 1, 'pageSize' => 500]; try { - $data = $this->request("/websites/ssl/search", $params); - $this->log('获取证书列表成功(total=' . $data['total'] . ')'); + $data = $this->request("/websites/ssl/search", $params, $nodeName); + $logMsg = $nodeName ? "节点 [{$nodeName}] " : ""; + $this->log($logMsg . '获取证书列表成功(total=' . $data['total'] . ')'); } catch (Exception $e) { - throw new Exception('获取证书列表失败:' . $e->getMessage()); + $logMsg = $nodeName ? "节点 [{$nodeName}] " : ""; + throw new Exception($logMsg . '获取证书列表失败:' . $e->getMessage()); } $success = 0; @@ -99,12 +155,14 @@ class opanel implements DeployInterface 'description' => '', ]; try { - $this->request('/websites/ssl/upload', $params); - $this->log("证书ID:{$row['id']}更新成功!"); + $this->request('/websites/ssl/upload', $params, $nodeName); + $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$row['id']}更新成功!" : "证书ID:{$row['id']}更新成功!"; + $this->log($logMsg); $success++; } catch (Exception $e) { $errmsg = $e->getMessage(); - $this->log("证书ID:{$row['id']}更新失败:" . $errmsg); + $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$row['id']}更新失败:" : "证书ID:{$row['id']}更新失败:"; + $this->log($logMsg . $errmsg); } } } @@ -117,8 +175,9 @@ class opanel implements DeployInterface 'privateKey' => $privatekey, 'description' => '', ]; - $this->request('/websites/ssl/upload', $params); - $this->log("证书上传成功!"); + $this->request('/websites/ssl/upload', $params, $nodeName); + $logMsg = $nodeName ? "节点 [{$nodeName}] 证书上传成功!" : "证书上传成功!"; + $this->log($logMsg); } } @@ -134,7 +193,32 @@ class opanel implements DeployInterface } } - private function request($path, $params = null) + /** + * 解析节点名称列表 + */ + private function parseNodeNames($config) + { + if (!isset($config['node_name']) || empty($config['node_name'])) { + return []; + } + + $nodeNameStr = trim($config['node_name']); + if (empty($nodeNameStr)) { + return []; + } + + // 按行分割,过滤空行 + $nodeNames = array_filter( + array_map('trim', explode("\n", $nodeNameStr)), + function($name) { + return !empty($name); + } + ); + + return array_values($nodeNames); + } + + private function request($path, $params = null, $nodeName = null) { $url = $this->url . $path; @@ -144,8 +228,8 @@ class opanel implements DeployInterface '1Panel-Token' => $token, '1Panel-Timestamp' => $timestamp, ]; - if (!empty($this->nodeName)) { - $headers['CurrentNode'] = $this->nodeName; + if (!empty($nodeName)) { + $headers['CurrentNode'] = $nodeName; } $body = $params ? json_encode($params) : '{}'; if ($body) $headers['Content-Type'] = 'application/json'; From b19cabcbfd5fc1a71c47a9bbc287310309c9d762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Wed, 24 Dec 2025 22:09:48 +0800 Subject: [PATCH 2/4] fix: Passing null to parameter #5 ($passphrase) of type string is deprecated (#360) --- app/lib/deploy/ssh.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/lib/deploy/ssh.php b/app/lib/deploy/ssh.php index af61df9..bd964c0 100644 --- a/app/lib/deploy/ssh.php +++ b/app/lib/deploy/ssh.php @@ -159,9 +159,14 @@ class ssh implements DeployInterface file_put_contents($privateKeyPath, $this->config['privatekey']); file_put_contents($publicKeyPath, $publicKey); umask($umask); - $passphrase = $this->config['passphrase'] ?? null; // 私钥密码 - if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $passphrase)) { - throw new Exception('私钥认证失败'); + if (!empty($this->config['passphrase'])) { + if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $this->config['passphrase'])) { + throw new Exception('私钥认证失败'); + } + } else { + if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) { + throw new Exception('私钥认证失败'); + } } } else { if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) { From ebdc34cf4b3f9bb4ee31a90b5f80e6f5907f3ef4 Mon Sep 17 00:00:00 2001 From: net909 Date: Thu, 25 Dec 2025 10:27:28 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E5=8F=88=E6=8B=8D=E4=BA=91SSL?= =?UTF-8?q?=E4=B8=8D=E5=85=BC=E5=AE=B9=E7=9A=84=E7=89=B9=E5=8C=96=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controller/Cert.php | 8 -------- app/lib/CertHelper.php | 18 ------------------ app/lib/acme/ACMECert.php | 5 +---- app/lib/deploy/upyun.php | 26 +++++++++++++++++++++++++- 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/app/controller/Cert.php b/app/controller/Cert.php index 33550cd..1693395 100644 --- a/app/controller/Cert.php +++ b/app/controller/Cert.php @@ -304,10 +304,6 @@ class Cert extends BaseController } } - if ($certInfo['keytype'] == 'ECC') { - $privatekey = CertHelper::ensureECPrivateKeyFormat($privatekey); - } - $order = [ 'aid' => 0, 'keytype' => $certInfo['keytype'], @@ -371,10 +367,6 @@ class Cert extends BaseController if ($certInfo['code'] == -1) return json($certInfo); $domains = $certInfo['domains']; - if ($certInfo['keytype'] == 'ECC') { - $privatekey = CertHelper::ensureECPrivateKeyFormat($privatekey); - } - $order = [ 'aid' => 0, 'keytype' => $certInfo['keytype'], diff --git a/app/lib/CertHelper.php b/app/lib/CertHelper.php index 7b3c3db..fa8f3ca 100644 --- a/app/lib/CertHelper.php +++ b/app/lib/CertHelper.php @@ -407,24 +407,6 @@ location / { return false; } - /** - * 确保ECC私钥使用EC专用格式标识 - * 某些程序需要EC标识才能正确识别ECC私钥 - */ - public static function ensureECPrivateKeyFormat($private_key) - { - if (strpos($private_key, '-----BEGIN EC PRIVATE KEY-----') !== false) { - return $private_key; - } - - if (strpos($private_key, '-----BEGIN PRIVATE KEY-----') !== false) { - $private_key = preg_replace('/^-----BEGIN PRIVATE KEY-----$/m', '-----BEGIN EC PRIVATE KEY-----', $private_key); - $private_key = preg_replace('/^-----END PRIVATE KEY-----$/m', '-----END EC PRIVATE KEY-----', $private_key); - } - - return $private_key; - } - public static function getPfx($fullchain, $privatekey, $pwd = '123456') { openssl_pkcs12_export($fullchain, $pfx, $privatekey, $pwd); diff --git a/app/lib/acme/ACMECert.php b/app/lib/acme/ACMECert.php index 1eada49..5c7ab27 100644 --- a/app/lib/acme/ACMECert.php +++ b/app/lib/acme/ACMECert.php @@ -4,7 +4,6 @@ namespace app\lib\acme; use Exception; use stdClass; -use app\lib\CertHelper; /** * ACMECert @@ -369,12 +368,10 @@ class ACMECert extends ACMEv2 if (version_compare(PHP_VERSION, '7.1.0') < 0) throw new Exception('PHP >= 7.1.0 required for EC keys !'); $map = array('256' => 'prime256v1', '384' => 'secp384r1', '521' => 'secp521r1'); if (isset($map[$curve_name])) $curve_name = $map[$curve_name]; - $pem = $this->generateKey(array( + return $this->generateKey(array( 'curve_name' => $curve_name, 'private_key_type' => OPENSSL_KEYTYPE_EC )); - - return CertHelper::ensureECPrivateKeyFormat($pem); } public function parseCertificate($cert_pem) diff --git a/app/lib/deploy/upyun.php b/app/lib/deploy/upyun.php index 77ae4e9..287689c 100644 --- a/app/lib/deploy/upyun.php +++ b/app/lib/deploy/upyun.php @@ -31,9 +31,15 @@ class upyun implements DeployInterface $this->login(); $url = 'https://console.upyun.com/api/https/certificate/'; + // 如果是 EC 证书,调整私钥头为 EC PRIVATE KEY + $privatekey_send = $privatekey; + if ($this->isEcCertificate($fullchain)) { + $privatekey_send = str_replace('-----BEGIN PRIVATE KEY-----', '-----BEGIN EC PRIVATE KEY-----', $privatekey_send); + $privatekey_send = str_replace('-----END PRIVATE KEY-----', '-----END EC PRIVATE KEY-----', $privatekey_send); + } $params = [ 'certificate' => $fullchain, - 'private_key' => $privatekey, + 'private_key' => $privatekey_send, ]; $response = http_request($url, http_build_query($params), null, $this->cookie, null, $this->proxy); $result = json_decode($response['body'], true); @@ -130,4 +136,22 @@ class upyun implements DeployInterface call_user_func($this->logger, $txt); } } + + /** + * 判断是否为 EC (ECDSA) 证书 + */ + private function isEcCertificate($fullchain) + { + // 提取第一个证书 + if (!preg_match('/-----BEGIN CERTIFICATE-----\s*(.+?)\s*-----END CERTIFICATE-----/s', $fullchain, $m)) { + return false; + } + + $pubKey = openssl_pkey_get_public($m[0]); + if (!$pubKey) return false; + + $details = openssl_pkey_get_details($pubKey); + + return $details && ($details['type'] ?? 0) === OPENSSL_KEYTYPE_EC; + } } From d0eb096873ebdc0fa5e9fce191aadd6233fb081d Mon Sep 17 00:00:00 2001 From: net909 Date: Thu, 25 Dec 2025 11:49:07 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E8=85=BE=E8=AE=AF=E4=BA=91=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=9B=B4=E6=96=B0=E8=AF=81=E4=B9=A6=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/DeployHelper.php | 9 ++++ app/lib/deploy/tencent.php | 92 ++++++++++++++++++++++++++++++++++++++ config/app.php | 2 +- 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/app/lib/DeployHelper.php b/app/lib/DeployHelper.php index 2427ece..e08d3ca 100644 --- a/app/lib/DeployHelper.php +++ b/app/lib/DeployHelper.php @@ -1224,6 +1224,7 @@ ctrl+x 保存退出', ['value'=>'tse', 'label'=>'云原生API网关TSE'], ['value'=>'tcb', 'label'=>'云开发TCB'], ['value'=>'lighthouse', 'label'=>'轻量应用服务器'], + ['value'=>'update', 'label'=>'更新证书内容(证书ID不变)'], ], 'value' => 'cdn', 'required' => true, @@ -1327,6 +1328,14 @@ ctrl+x 保存退出', 'note' => 'CDN、EO、WAF多个域名可用,隔开,其他只能填写1个域名', 'required' => true, ], + 'cert_id' => [ + 'name' => '证书ID', + 'type' => 'input', + 'placeholder' => '要更新的证书ID,在我的证书列表查看', + 'show' => 'product==\'update\'', + 'required' => true, + 'note' => '当前接口需联系加白使用', + ], ], ], 'huawei' => [ diff --git a/app/lib/deploy/tencent.php b/app/lib/deploy/tencent.php index f290288..91f2d7c 100644 --- a/app/lib/deploy/tencent.php +++ b/app/lib/deploy/tencent.php @@ -31,6 +31,9 @@ class tencent implements DeployInterface public function deploy($fullchain, $privatekey, $config, &$info) { + if ($config['product'] == 'update') { + return $this->update_cert($fullchain, $privatekey, $config); + } $cert_id = $this->get_cert_id($fullchain, $privatekey); if (!$cert_id) throw new Exception('证书ID获取失败'); if ($config['product'] == 'cos') { @@ -281,6 +284,95 @@ class tencent implements DeployInterface $this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!'); } + private function update_cert($fullchain, $privatekey, $config) + { + if (empty($config['cert_id'])) throw new Exception('证书ID不能为空'); + + $param = [ + 'CertificateIds' => [$config['cert_id']], + 'IsCache' => 1, + ]; + try { + $data = $this->client->request('CreateCertificateBindResourceSyncTask', $param); + if (empty($data['CertTaskIds'])) throw new Exception('返回任务ID为空'); + } catch (Exception $e) { + throw new Exception('创建关联云资源查询任务失败:' . $e->getMessage()); + } + $task_id = $data['CertTaskIds'][0]['TaskId']; + $this->log('创建关联云资源查询任务成功 TaskId=' . $task_id); + + $retry = 0; + $resource_result = null; + while ($retry++ < 30) { + sleep(2); + $param = [ + 'TaskIds' => [$task_id], + ]; + try { + $data = $this->client->request('DescribeCertificateBindResourceTaskResult', $param); + if (empty($data['SyncTaskBindResourceResult'])) throw new Exception('返回结果为空'); + } catch (Exception $e) { + throw new Exception('查询关联云资源任务结果失败:' . $e->getMessage()); + } + $taskResult = $data['SyncTaskBindResourceResult'][0]; + if ($taskResult['Status'] == 1) { + $resource_result = $taskResult['BindResourceResult']; + break; + } elseif ($taskResult['Status'] == 2) { + throw new Exception('关联云资源查询任务执行失败:' . isset($taskResult['Error']) ? $taskResult['Error']['Message'] : '未知错误'); + } + }; + if (!$resource_result) { + throw new Exception('关联云资源查询任务超时未完成,请稍后重试'); + } + + $resourceTypes = []; + $resourceTypesRegions = []; + foreach ($resource_result as $res) { + if ($res['ResourceType'] != 'clb') continue; + $totalCount = 0; + $regions = []; + foreach ($res['BindResourceRegionResult'] as $regionRes) { + if ($regionRes['TotalCount'] > 0) { + $totalCount += $regionRes['TotalCount']; + if (!empty($regionRes['Region'])) { + $regions[] = $regionRes['Region']; + } + } + } + if ($totalCount > 0) { + $resourceTypes[] = $res['ResourceType']; + if (!empty($regions)) { + $resourceTypesRegions[] = [ + 'ResourceType' => $res['ResourceType'], + 'Regions' => $regions, + ]; + } + } + } + + $param = [ + 'OldCertificateId' => $config['cert_id'], + 'CertificatePublicKey' => $fullchain, + 'CertificatePrivateKey' => $privatekey, + 'ResourceTypes' => $resourceTypes, + 'ResourceTypesRegions' => $resourceTypesRegions, + ]; + $retry = 0; + while ($retry++ < 10) { + try { + $data = $this->client->request('UploadUpdateCertificateInstance', $param); + } catch (Exception $e) { + throw new Exception('更新证书内容失败:' . $e->getMessage()); + } + if ($data['DeployStatus'] == 1) { + break; + } + sleep(1); + } + $this->log('更新证书内容成功,可能需要一些时间完成各资源的证书更新部署'); + } + public function setLogger($func) { $this->logger = $func; diff --git a/config/app.php b/config/app.php index c81ffc9..9b89298 100644 --- a/config/app.php +++ b/config/app.php @@ -31,7 +31,7 @@ return [ 'show_error_msg' => true, 'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl', - 'version' => '1043', + 'version' => '1044', 'dbversion' => '1040' ];