url = rtrim($config['url'], '/') . '/api/' . (isset($config['version']) ? $config['version'] : 'v1'); $this->key = $config['key']; $this->proxy = $config['proxy'] == 1; } public function check() { if (empty($this->url) || empty($this->key)) throw new Exception('请填写面板地址和接口密钥'); $this->request("/settings/search"); } public function deploy($fullchain, $privatekey, $config, &$info) { // 解析节点名称列表 $nodeNames = $this->parseNodeNames($config); if (isset($config['type']) && $config['type'] == '3') { // 面板本身的证书部署 $params = [ 'cert' => $fullchain, 'key' => $privatekey, 'ssl' => 'Enable', 'sslID' => null, 'sslType' => 'import-paste', ]; 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; // 先更新主节点 try { $this->request('/core/settings/ssl/update', $params); $this->log("主节点面板证书更新成功!"); $successCount++; } catch (Exception $e) { $this->log("主节点面板证书更新失败:" . $e->getMessage()); $failCount++; } // 然后更新所有子节点 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; } } // 如果没有指定节点,则只部署到主控节点 if (empty($nodeNames)) { $this->deployToNode($fullchain, $privatekey, $config, null); } else { // 同时部署到主节点和所有指定的子节点 $successCount = 0; $failCount = 0; // 先更新主节点 try { $this->deployToNode($fullchain, $privatekey, $config, null); $successCount++; } catch (Exception $e) { $this->log("主节点部署失败:" . $e->getMessage()); $failCount++; } // 然后更新所有子节点 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', 'certificate' => $fullchain, 'privateKey' => $privatekey, 'description' => '', ]; try { $this->request('/websites/ssl/upload', $params, $nodeName); $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$config['id']}更新成功!" : "证书ID:{$config['id']}更新成功!"; $this->log($logMsg); return; } catch (Exception $e) { $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, $nodeName); $logMsg = $nodeName ? "节点 [{$nodeName}] " : ""; $this->log($logMsg . '获取证书列表成功(total=' . $data['total'] . ')'); } catch (Exception $e) { $logMsg = $nodeName ? "节点 [{$nodeName}] " : ""; throw new Exception($logMsg . '获取证书列表失败:' . $e->getMessage()); } $success = 0; $errmsg = null; if (!empty($data['items'])) { foreach ($data['items'] as $row) { if (empty($row['primaryDomain'])) continue; $cert_domains = []; $cert_domains[] = $row['primaryDomain']; if (!empty($row['domains'])) $cert_domains += explode(',', $row['domains']); $flag = false; foreach ($cert_domains as $domain) { if (in_array($domain, $domains) || in_array('*' . substr($domain, strpos($domain, '.')), $domains)) { $flag = true; break; } } if ($flag) { $params = [ 'sslID' => $row['id'], 'type' => 'paste', 'certificate' => $fullchain, 'privateKey' => $privatekey, 'description' => '', ]; try { $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(); $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$row['id']}更新失败:" : "证书ID:{$row['id']}更新失败:"; $this->log($logMsg . $errmsg); } } } } if ($success == 0) { $params = [ 'sslID' => 0, 'type' => 'paste', 'certificate' => $fullchain, 'privateKey' => $privatekey, 'description' => '', ]; $this->request('/websites/ssl/upload', $params, $nodeName); $logMsg = $nodeName ? "节点 [{$nodeName}] 证书上传成功!" : "证书上传成功!"; $this->log($logMsg); } } public function setLogger($func) { $this->logger = $func; } private function log($txt) { if ($this->logger) { call_user_func($this->logger, $txt); } } /** * 解析节点名称列表 */ 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; $timestamp = time() . ''; $token = md5('1panel' . $this->key . $timestamp); $headers = [ '1Panel-Token' => $token, '1Panel-Timestamp' => $timestamp, ]; // 只有子节点时才设置 CurrentNode 头,主节点时不设置该头 if (!empty($nodeName)) { $headers['CurrentNode'] = $nodeName; } $body = $params ? json_encode($params) : '{}'; if ($body) $headers['Content-Type'] = 'application/json'; $response = http_request($url, $body, null, null, $headers, $this->proxy); $result = json_decode($response['body'], true); if (isset($result['code']) && $result['code'] == 200) { return $result['data'] ?? null; } elseif (isset($result['message'])) { throw new Exception($result['message']); } else { throw new Exception('请求失败(httpCode=' . $response['code'] . ')'); } } }