From 9b7a7c2d60fae9c782688523984cac9d845932e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Fri, 16 May 2025 00:48:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=80=97=E5=AD=90=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/DeployHelper.php | 58 +++++++++++ app/lib/deploy/ratpanel.php | 163 ++++++++++++++++++++++++++++++ public/static/images/ratpanel.ico | Bin 0 -> 15406 bytes 3 files changed, 221 insertions(+) create mode 100644 app/lib/deploy/ratpanel.php create mode 100755 public/static/images/ratpanel.ico diff --git a/app/lib/DeployHelper.php b/app/lib/DeployHelper.php index 3abb493..8431800 100644 --- a/app/lib/DeployHelper.php +++ b/app/lib/DeployHelper.php @@ -466,6 +466,64 @@ class DeployHelper ], ], ], + 'ratpanel' => [ + 'name' => '耗子面板', + 'class' => 1, + 'icon' => 'ratpanel.ico', + 'note' => '支持耗子面板 v2.5+ 版本使用', + 'inputs' => [ + 'url' => [ + 'name' => '面板地址', + 'type' => 'input', + 'placeholder' => '耗子面板地址', + 'note' => '填写规则如:https://192.168.1.100:8888/xxxxxx ,带访问入口但不要带其他后缀', + 'required' => true, + ], + 'id' => [ + 'name' => '访问令牌ID', + 'type' => 'input', + 'placeholder' => '1', + 'note' => '耗子面板设置->用户->访问令牌', + 'required' => true, + ], + 'token' => [ + 'name' => '访问令牌', + 'type' => 'input', + 'note' => '耗子面板设置->用户->访问令牌', + 'placeholder' => '32位字符串', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'type' => [ + 'name' => '部署类型', + 'type' => 'radio', + 'options' => [ + '0' => '耗子面板网站的证书', + '1' => '耗子面板本身的证书', + ], + 'value' => '0', + 'required' => true, + ], + 'sites' => [ + 'name' => '网站名称列表', + 'type' => 'textarea', + 'placeholder' => '填写要部署证书的网站名称,每行一个', + 'note' => '填写创建网站时设置的网站唯一名称', + 'show' => 'type==0', + 'required' => true, + ], + ], + ], 'synology' => [ 'name' => '群晖面板', 'class' => 1, diff --git a/app/lib/deploy/ratpanel.php b/app/lib/deploy/ratpanel.php new file mode 100644 index 0000000..9a215d1 --- /dev/null +++ b/app/lib/deploy/ratpanel.php @@ -0,0 +1,163 @@ +url = rtrim($config['url'], '/'); + $this->id = $config['id']; + $this->token = $config['token']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->id) || empty($this->token)) throw new Exception('请填写完整面板地址和访问令牌'); + + $response = $this->request('/user/info', null, 'GET'); + $result = json_decode($response, true); + if (isset($result['msg']) && $result['msg'] == "success") { + return true; + } else { + throw new Exception($result['msg'] ?? '面板地址无法连接'); + } + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if ($config['type'] == '1') { + $this->deployPanel($fullchain, $privatekey); + $this->log("面板证书部署成功"); + return; + } + $sites = explode("\n", $config['sites']); + $success = 0; + $errmsg = null; + foreach ($sites as $site) { + $site = trim($site); + if (empty($site)) continue; + try { + $this->deploySite($site, $fullchain, $privatekey); + $this->log("网站 {$site} 证书部署成功"); + $success++; + } catch (Exception $e) { + $errmsg = $e->getMessage(); + $this->log("网站 {$site} 证书部署失败:" . $errmsg); + } + } + if ($success == 0) { + throw new Exception($errmsg ?: '要部署的网站不存在'); + } + } + + private function deployPanel($fullchain, $privatekey) + { + $data = [ + 'cert' => $fullchain, + 'key' => $privatekey, + ]; + $response = $this->request('/setting/cert', $data); + $result = json_decode($response, true); + if (isset($result['msg']) && $result['msg'] == "success") { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ?: '返回数据解析失败'); + } + } + + private function deploySite($name, $fullchain, $privatekey) + { + $data = [ + 'name' => $name, + 'cert' => $fullchain, + 'key' => $privatekey, + ]; + $response = $this->request('/website/cert', $data); + $result = json_decode($response, true); + if (isset($result['msg']) && $result['msg'] == "success") { + return true; + } elseif (isset($result['msg'])) { + throw new Exception($result['msg']); + } else { + throw new Exception($response ?: '返回数据解析失败'); + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($path, $params, $method = 'POST') + { + $url = $this->url . '/api' . $path; + $body = $method == 'GET' ? null : json_encode($params); + $sign = $this->signRequest($method, $url, $body, $this->id, $this->token); + $response = curl_client($url, $body, null, null, [ + 'Content-Type: application/json', + 'X-Timestamp: ' . $sign['timestamp'], + 'Authorization: HMAC-SHA256 Credential=' . $sign['id'] . ', Signature=' . $sign['signature'] + ], $this->proxy, $method); + return $response['body']; + } + + private function signRequest($method, $url, $body, $id, $token) + { + // 解析URL并获取路径 + $parsedUrl = parse_url($url); + $path = $parsedUrl['path']; + $query = $parsedUrl['query'] ?? ''; + + // 规范化路径 + $canonicalPath = $path; + if (strpos($path, '/api') !== 0) { + $apiPos = strpos($path, '/api'); + if ($apiPos !== false) { + $canonicalPath = substr($path, $apiPos); + } + } + + // 构造规范化请求 + $canonicalRequest = implode("\n", [ + $method, + $canonicalPath, + $query, + hash('sha256', $body ?: '') + ]); + + // 计算签名 + $timestamp = time(); + $stringToSign = implode("\n", [ + 'HMAC-SHA256', + $timestamp, + hash('sha256', $canonicalRequest) + ]); + $signature = hash_hmac('sha256', $stringToSign, $token); + + return [ + 'timestamp' => $timestamp, + 'signature' => $signature, + 'id' => $id + ]; + } +} diff --git a/public/static/images/ratpanel.ico b/public/static/images/ratpanel.ico new file mode 100755 index 0000000000000000000000000000000000000000..9161b3ebfc0fa5bacf5c737ef9edc8f4b5217e9b GIT binary patch literal 15406 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 literal 0 HcmV?d00001