diff --git a/app/controller/System.php b/app/controller/System.php index 0e2ebf3..3b9597a 100644 --- a/app/controller/System.php +++ b/app/controller/System.php @@ -95,11 +95,12 @@ class System extends BaseController public function proxytest() { if (!checkPermission(2)) return $this->alert('error', '无权限'); - $proxy_server = trim($_POST['proxy_server']); - $proxy_port = $_POST['proxy_port']; - $proxy_user = trim($_POST['proxy_user']); - $proxy_pwd = trim($_POST['proxy_pwd']); - $proxy_type = $_POST['proxy_type']; + $proxy_server = input('post.proxy_server', '', 'trim'); + $proxy_port = input('post.proxy_port/d', 0); + $proxy_user = input('post.proxy_user', '', 'trim'); + $proxy_pwd = input('post.proxy_pwd', '', 'trim'); + $proxy_type = input('post.proxy_type', 'http', 'trim'); + try { check_proxy('https://dl.amh.sh/ip.htm', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd); } catch (Exception $e) { diff --git a/app/lib/DeployHelper.php b/app/lib/DeployHelper.php index f26c253..f98099f 100644 --- a/app/lib/DeployHelper.php +++ b/app/lib/DeployHelper.php @@ -677,6 +677,65 @@ class DeployHelper ], ], ], + 'acepanel' => [ + 'name' => 'AcePanel', + 'class' => 1, + 'icon' => 'acepanel.svg', + 'desc' => '支持 AcePanel 3.0+ 版本使用', + 'note' => '支持 AcePanel 3.0+ 版本使用', + 'inputs' => [ + 'url' => [ + 'name' => '面板地址', + 'type' => 'input', + 'placeholder' => 'AcePanel 地址', + 'note' => '填写规则如:https://192.168.1.100:8888/xxxxxx ,带访问入口但不要带其他后缀', + 'required' => true, + ], + 'id' => [ + 'name' => '访问令牌ID', + 'type' => 'input', + 'placeholder' => '1', + 'note' => 'AcePanel 设置->用户->访问令牌', + 'required' => true, + ], + 'token' => [ + 'name' => '访问令牌', + 'type' => 'input', + 'note' => 'AcePanel 设置->用户->访问令牌', + 'placeholder' => '32位字符串', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'type' => [ + 'name' => '部署类型', + 'type' => 'radio', + 'options' => [ + '0' => 'AcePanel 网站的证书', + '1' => 'AcePanel 本身的证书', + ], + 'value' => '0', + 'required' => true, + ], + 'sites' => [ + 'name' => '网站名称列表', + 'type' => 'textarea', + 'placeholder' => '填写要部署证书的网站名称,每行一个', + 'note' => '填写创建网站时设置的网站唯一名称', + 'show' => 'type==0', + 'required' => true, + ], + ], + ], 'ratpanel' => [ 'name' => '耗子面板', 'class' => 1, @@ -777,6 +836,53 @@ class DeployHelper ], ], ], + 'amh' => [ + 'name' => 'AMH面板', + 'class' => 1, + 'icon' => 'amh.ico', + 'desc' => '', + 'note' => null, + 'tasknote' => '', + 'inputs' => [ + 'url' => [ + 'name' => '面板地址', + 'type' => 'input', + 'placeholder' => 'AMH面板地址', + 'note' => '填写规则如:http://192.168.1.100:8888 ,不要带其他后缀', + 'required' => true, + ], + 'apikey' => [ + 'name' => 'API接口密钥', + 'type' => 'input', + 'placeholder' => '安装amapi软件后查看,是密钥不是私钥', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'env_name' => [ + 'name' => '环境名称', + 'type' => 'input', + 'placeholder' => '如:lnmp01', + 'required' => true, + ], + 'vhost_name' => [ + 'name' => '网站名称列表', + 'type' => 'textarea', + 'placeholder' => '填写要部署证书的网站标识域名,每行一个', + 'note' => '网站标识域名一列的值,并非绑定域名', + 'required' => true, + ], + ], + ], 'synology' => [ 'name' => '群晖面板', 'class' => 1, diff --git a/app/lib/deploy/acepanel.php b/app/lib/deploy/acepanel.php new file mode 100644 index 0000000..78d8252 --- /dev/null +++ b/app/lib/deploy/acepanel.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 = http_request($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 (!str_starts_with($path, '/api')) { + $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/app/lib/deploy/amh.php b/app/lib/deploy/amh.php new file mode 100644 index 0000000..88771b8 --- /dev/null +++ b/app/lib/deploy/amh.php @@ -0,0 +1,108 @@ +url = rtrim($config['url'], '/'); + $this->apikey = $config['apikey']; + $this->proxy = $config['proxy'] == 1; + } + + public function check() + { + if (empty($this->url) || empty($this->apikey)) throw new Exception('请填写面板地址和接口密钥'); + $this->login(); + return true; + } + + private function login() + { + $path = '/?c=amapi&a=login'; + $post_data = 'amapi_expires=' . time() + 120; + $post_data .= '&amapi_sign=' . hash_hmac('sha256', $post_data, $this->apikey); + $response = $this->request($path, $post_data); + if ($response['code'] == 302 && strpos($response['redirect_url'], 'amh_token=') !== false) { + if(preg_match('/amh_token=([A-Za-z0-9]+)/', $response['redirect_url'], $matches)) { + return $matches[1]; + }else{ + throw new Exception('面板返回数据异常'); + } + } elseif ($response['code'] == 200 && preg_match('/

(.*?)<\/p>/s', $response['body'], $matches)) { + throw new Exception(strip_tags($matches[1])); + } else { + throw new Exception('面板地址无法连接'); + } + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + if (empty($config['env_name'])) throw new Exception('环境名称不能为空'); + if (empty($config['vhost_name'])) throw new Exception('网站标识域名不能为空'); + + $amh_token = $this->login(); + + foreach (explode("\n", $config['vhost_name']) as $vhost_name) { + $vhost_name = trim($vhost_name); + if (empty($vhost_name)) continue; + + $path = '/?c=amssl&a=admin_amssl&envs_name=' . $config['env_name'] . '&vhost_name=' . $vhost_name . '&ModuleSort=app'; + $params = [ + 'submit_key_crt' => 'y', + 'key_input1' => 'key_input1', + 'key_content1' => $privatekey, + 'crt_input1' => 'crt_input1', + 'crt_content1' => $fullchain, + 'amh_token' => $amh_token, + ]; + $response = $this->request($path, $params); + if (strpos($response['body'], '

log("网站 {$vhost_name} 证书部署成功"); + } elseif (preg_match('/

(.*?)<\/p>/s', $response['body'], $matches)) { + $errmsg = strip_tags($matches[1]); + $this->log("网站 {$vhost_name} 证书部署失败:" . $errmsg); + throw new Exception($errmsg); + } elseif (preg_match('/

(.*?)
/s', $response['body'], $matches)) { + $errmsg = $matches[1]; + if (strpos($errmsg, '
') !== false) { + $errmsg = explode('
', $errmsg)[0]; + } + $errmsg = strip_tags($errmsg); + $this->log("网站 {$vhost_name} 证书部署失败:" . $errmsg); + throw new Exception($errmsg); + } else { + throw new Exception("网站 {$vhost_name} 证书部署失败:未知错误"); + } + } + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } + + private function request($path, $post_data = null) + { + $url = $this->url . $path; + $cookie = 'PHPSESSID=' . hash_hmac('md5', 'php_sessid=' . $this->apikey, $this->apikey); + $response = http_request($url, $post_data, null, $cookie, null, $this->proxy); + return $response; + } +} diff --git a/app/lib/deploy/ssh.php b/app/lib/deploy/ssh.php index bd964c0..88a1501 100644 --- a/app/lib/deploy/ssh.php +++ b/app/lib/deploy/ssh.php @@ -159,14 +159,20 @@ class ssh implements DeployInterface file_put_contents($privateKeyPath, $this->config['privatekey']); file_put_contents($publicKeyPath, $publicKey); umask($umask); - 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('私钥认证失败'); + + try { + 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('私钥认证失败'); + } } + } finally { + unlink($publicKeyPath); + unlink($privateKeyPath); } } else { if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) { diff --git a/public/static/images/acepanel.svg b/public/static/images/acepanel.svg new file mode 100644 index 0000000..b3da3f7 --- /dev/null +++ b/public/static/images/acepanel.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/static/images/amh.ico b/public/static/images/amh.ico new file mode 100644 index 0000000..2f610db Binary files /dev/null and b/public/static/images/amh.ico differ