diff --git a/app/lib/DeployHelper.php b/app/lib/DeployHelper.php index df4b35f..1e8cc3a 100644 --- a/app/lib/DeployHelper.php +++ b/app/lib/DeployHelper.php @@ -667,7 +667,7 @@ class DeployHelper 'name' => '实例ID', 'type' => 'input', 'placeholder' => '', - 'show' => 'product==\'lighthouse\'', + 'show' => 'product==\'lighthouse\'||product==\'ddos\'', 'required' => true, ], 'domain' => [ @@ -816,10 +816,18 @@ class DeployHelper 'options' => [ ['value'=>'cdn', 'label'=>'CDN'], ['value'=>'oss', 'label'=>'OSS'], + ['value'=>'pili', 'label'=>'视频直播'], ], 'value' => 'cdn', 'required' => true, ], + 'pili_hub' => [ + 'name' => '直播空间名称', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'pili\'', + 'required' => true, + ], 'domain' => [ 'name' => '绑定的域名', 'type' => 'input', @@ -931,10 +939,39 @@ class DeployHelper ], ], 'taskinputs' => [ + 'product' => [ + 'name' => '要部署的产品', + 'type' => 'select', + 'options' => [ + ['value'=>'cdn', 'label'=>'内容分发网络CDN'], + ['value'=>'dcdn', 'label'=>'全站加速DCDN'], + ['value'=>'clb', 'label'=>'负载均衡CLB'], + ['value'=>'tos', 'label'=>'对象存储TOS'], + ['value'=>'live', 'label'=>'视频直播'], + ['value'=>'imagex', 'label'=>'veImageX'], + ], + 'value' => 'cdn', + 'required' => true, + ], + 'bucket_domain' => [ + 'name' => 'Bucket域名', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'tos\'', + 'required' => true, + ], 'domain' => [ 'name' => '绑定的域名', 'type' => 'input', 'placeholder' => '多个域名可使用,分隔', + 'show' => 'product!=\'clb\'', + 'required' => true, + ], + 'listener_id' => [ + 'name' => '监听器ID', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'clb\'', 'required' => true, ], ], diff --git a/app/lib/acme/ACMECert.php b/app/lib/acme/ACMECert.php index 592e36d..db6e275 100644 --- a/app/lib/acme/ACMECert.php +++ b/app/lib/acme/ACMECert.php @@ -227,7 +227,7 @@ class ACMECert extends ACMEv2 public function authOrder($order) { - if ($order['status'] != 'ready' && empty($order['challenges'])) { + if ($order['status'] != 'pending' && $order['status'] != 'ready' && empty($order['challenges'])) { throw new Exception('No challenges available'); } diff --git a/app/lib/client/Qiniu.php b/app/lib/client/Qiniu.php index 3d2680f..6ae17f7 100644 --- a/app/lib/client/Qiniu.php +++ b/app/lib/client/Qiniu.php @@ -61,6 +61,39 @@ class Qiniu return $this->curl($method, $url, $body, $header); } + public function pili_request($method, $path, $query = null, $params = null) + { + $this->ApiUrl = 'https://pili.qiniuapi.com'; + $url = $this->ApiUrl . $path; + $query_str = null; + $body = null; + if (!empty($query)) { + $query = array_filter($query, function ($a) { + return $a !== null; + }); + $query_str = http_build_query($query); + $url .= '?' . $query_str; + } + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + $body = json_encode($params); + } + + $sign_str = $method . ' ' . $path . ($query_str ? '?' . $query_str : '') . "\nHost: pili.qiniuapi.com" . ($body ? "\nContent-Type: application/json" : '') . "\n\n" . $body; + $hmac = hash_hmac('sha1', $sign_str, $this->SecretKey, true); + $sign = $this->AccessKey . ':' . $this->base64_urlSafeEncode($hmac); + + $header = [ + 'Authorization: Qiniu ' . $sign, + ]; + if ($body) { + $header[] = 'Content-Type: application/json'; + } + return $this->curl($method, $url, $body, $header); + } + private function base64_urlSafeEncode($data) { $find = array('+', '/'); @@ -95,7 +128,7 @@ class Qiniu if ($httpCode == 200) { $arr = json_decode($response, true); - if($arr) return $arr; + if ($arr) return $arr; return true; } else { $arr = json_decode($response, true); diff --git a/app/lib/client/Volcengine.php b/app/lib/client/Volcengine.php index 90f4a16..f4f7d2c 100644 --- a/app/lib/client/Volcengine.php +++ b/app/lib/client/Volcengine.php @@ -38,7 +38,9 @@ class Volcengine public function request($method, $action, $params = [], $querys = []) { if (!empty($params)) { - $params = array_filter($params, function ($a) { return $a !== null;}); + $params = array_filter($params, function ($a) { + return $a !== null; + }); } $query = [ @@ -78,9 +80,51 @@ class Volcengine return $this->curl($method, $url, $body, $header); } + /** + * @param string $method 请求方法 + * @param string $action 方法名称 + * @param array $params 请求参数 + * @return array + * @throws Exception + */ + public function tos_request($method, $params = [], $query = []) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + $body = ''; + if ($method != 'GET') { + $body = !empty($params) ? json_encode($params) : ''; + } + + $time = time(); + $headers = [ + 'Host' => $this->endpoint, + 'X-Tos-Date' => gmdate("Ymd\THis\Z", $time), + 'X-Tos-Content-Sha256' => hash("sha256", $body), + ]; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + $path = '/'; + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); + $headers['Authorization'] = $authorization; + + $url = 'https://' . $this->endpoint . $path . '?' . http_build_query($query); + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + private function generateSign($method, $path, $query, $headers, $body, $time) { - $algorithm = "HMAC-SHA256"; + $algorithm = $this->service == 'tos' ? "TOS4-HMAC-SHA256" : "HMAC-SHA256"; // step 1: build canonical request string $httpRequestMethod = $method; @@ -177,21 +221,27 @@ class Volcengine curl_close($ch); throw new Exception('Curl error: ' . curl_error($ch)); } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $arr = json_decode($response, true); - if ($arr) { + if ($httpCode == 200) { + if (isset($arr['Result'])) { + return $arr['Result']; + } + return true; + } else { if (isset($arr['ResponseMetadata']['Error']['MessageCN'])) { throw new Exception($arr['ResponseMetadata']['Error']['MessageCN']); } elseif (isset($arr['ResponseMetadata']['Error']['Message'])) { throw new Exception($arr['ResponseMetadata']['Error']['Message']); - } elseif (isset($arr['Result'])) { - return $arr['Result']; + } elseif (isset($arr['Message'])) { + throw new Exception($arr['Message']); + } elseif (isset($arr['message'])) { + throw new Exception($arr['message']); } else { - return true; + throw new Exception('返回数据解析失败(http_code=' . $httpCode . ')'); } - } else { - throw new Exception('返回数据解析失败'); } } } diff --git a/app/lib/deploy/huoshan.php b/app/lib/deploy/huoshan.php index 9d00d9a..91e6eef 100644 --- a/app/lib/deploy/huoshan.php +++ b/app/lib/deploy/huoshan.php @@ -23,18 +23,31 @@ class huoshan implements DeployInterface public function check() { if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'cdn.volcengineapi.com', 'cdn', '2021-03-01', 'cn-north-1', $this->proxy); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'cdn', '2021-03-01', 'cn-north-1', $this->proxy); $client->request('POST', 'ListCertInfo', ['Source' => 'volc_cert_center']); return true; } public function deploy($fullchain, $privatekey, $config, &$info) { - if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $cert_id = $this->get_cert_id($fullchain, $privatekey); - if (!$cert_id) throw new Exception('获取证书ID失败'); - $info['cert_id'] = $cert_id; - $this->deploy_cdn($cert_id, $config); + if ($config['product'] == 'live') { + $this->deploy_live($fullchain, $privatekey, $config); + } else { + $cert_id = $this->get_cert_id($fullchain, $privatekey); + if (!$cert_id) throw new Exception('获取证书ID失败'); + $info['cert_id'] = $cert_id; + if (!isset($config['product']) || $config['product'] == 'cdn') { + $this->deploy_cdn($cert_id, $config); + } elseif ($config['product'] == 'dcdn') { + $this->deploy_dcdn($cert_id, $config); + } elseif ($config['product'] == 'tos') { + $this->deploy_tos($cert_id, $config); + } elseif ($config['product'] == 'imagex') { + $this->deploy_imagex($cert_id, $config); + } elseif ($config['product'] == 'clb') { + $this->deploy_clb($cert_id, $config); + } + } } private function deploy_cdn($cert_id, $config) @@ -51,37 +64,137 @@ class huoshan implements DeployInterface if ($row['Status'] == 'success') { $this->log('CDN域名 ' . $row['Domain'] . ' 部署证书成功!'); } else { - $this->log('CDN域名 ' . $row['Domain'] . ' 部署证书失败:' . isset($row['ErrorMsg']) ? $row['ErrorMsg'] : ''); + $this->log('CDN域名 ' . $row['Domain'] . ' 部署证书失败:' . (isset($row['ErrorMsg']) ? $row['ErrorMsg'] : '')); } } } + private function deploy_dcdn($cert_id, $config) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'dcdn', '2021-04-01', 'cn-north-1', $this->proxy); + $param = [ + 'CertId' => $cert_id, + 'DomainNames' => explode(',', $config['domain']), + ]; + $client->request('POST', 'CreateCertBind', $param); + $this->log('DCDN域名 ' . $config['domain'] . ' 部署证书成功!'); + } + + private function deploy_tos($cert_id, $config) + { + if (empty($config['bucket_domain'])) throw new Exception('Bucket域名不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, $config['bucket_domain'], 'tos', '2021-04-01', 'cn-beijing', $this->proxy); + foreach (explode(',', $config['domain']) as $domain) { + $param = [ + 'CustomDomainRule' => [ + 'Domain' => $domain, + 'CertId' => $cert_id, + ] + ]; + $query = ['customdomain' => '']; + $client->tos_request('PUT', $param, $query); + $this->log('对象存储域名 ' . $config['domain'] . ' 部署证书成功!'); + } + } + + private function deploy_live($fullchain, $privatekey, $config) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'live.volcengineapi.com', 'live', '2023-01-01', 'cn-north-1', $this->proxy); + $param = [ + 'CertName' => $cert_name, + 'Rsa' => [ + 'Pubkey' => $fullchain, + 'Prikey' => $privatekey, + ], + 'UseWay' => 'https', + ]; + $result = $client->request('POST', 'CreateCert', $param); + $this->log('上传证书成功 ChainID=' . $result['ChainID']); + + foreach (explode(',', $config['domain']) as $domain) { + $param = [ + 'ChainID' => $result['ChainID'], + 'Domain' => $domain, + 'HTTPS' => true, + 'HTTP2' => true, + ]; + $client->request('POST', 'BindCert', $param); + $this->log('视频直播域名 ' . $domain . ' 部署证书成功!'); + } + } + + private function deploy_imagex($cert_id, $config) + { + if (empty($config['bucket_domain'])) throw new Exception('Bucket域名不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'imagex.volcengineapi.com', 'imagex', '2018-08-01', 'cn-north-1', $this->proxy); + foreach (explode(',', $config['domain']) as $domain) { + $param = [ + [ + 'domain' => $domain, + 'cert_id' => $cert_id, + ] + ]; + $result = $client->request('POST', 'UpdateImageBatchDomainCert', $param); + if (isset($result['SuccessDomains']) && count($result['SuccessDomains']) > 0) { + $this->log('veImageX域名 ' . $domain . ' 部署证书成功!'); + } elseif (isset($result['FailedDomains']) && count($result['FailedDomains']) > 0) { + $errmsg = $result['FailedDomains'][0]['ErrMsg']; + $this->log('veImageX域名 ' . $domain . ' 部署证书失败:' . $errmsg); + } else { + $this->log('veImageX域名 ' . $domain . ' 部署证书失败'); + } + } + } + + private function deploy_clb($cert_id, $config) + { + if (empty($config['listener_id'])) throw new Exception('监听器ID不能为空'); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'clb', '2020-04-01', 'cn-beijing', $this->proxy); + $param = [ + 'ListenerId' => $config['listener_id'], + 'CertificateSource' => 'cert_center', + 'CertCenterCertificateId' => $cert_id, + ]; + $client->request('GET', 'ModifyListenerAttributes', $param); + $this->log('CLB监听器 ' . $config['listener_id'] . ' 部署证书成功!'); + } + private function get_cert_id($fullchain, $privatekey) { $certInfo = openssl_x509_parse($fullchain, true); if (!$certInfo) throw new Exception('证书解析失败'); $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'cdn.volcengineapi.com', 'cdn', '2021-03-01', 'cn-north-1', $this->proxy); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'certificate_service', '2024-10-01', 'cn-beijing', $this->proxy); $param = [ - 'Source' => 'volc_cert_center', - 'Certificate' => $fullchain, - 'PrivateKey' => $privatekey, - 'Desc' => $cert_name, + 'Tag' => $cert_name, 'Repeatable' => false, + 'CertificateInfo' => [ + 'CertificateChain' => $fullchain, + 'PrivateKey' => $privatekey, + ], ]; try { - $data = $client->request('POST', 'AddCertificate', $param); + $data = $client->request('POST', 'ImportCertificate', $param); } catch (Exception $e) { - if (strpos($e->getMessage(), '证书已存在,ID为') !== false) { - $cert_id = trim(getSubstr($e->getMessage(), '证书已存在,ID为', '。')); - $this->log('证书已存在 CertId=' . $cert_id); - return $cert_id; - } throw new Exception('上传证书失败:' . $e->getMessage()); } - $this->log('上传证书成功 CertId=' . $data['CertId']); - return $data['CertId']; + if (!empty($data['InstanceId'])) { + $cert_id = $data['InstanceId']; + } else { + $cert_id = $data['RepeatId']; + } + $this->log('上传证书成功 CertId=' . $cert_id); + return $cert_id; } public function setLogger($func) diff --git a/app/lib/deploy/qiniu.php b/app/lib/deploy/qiniu.php index 9abbaa6..b813d35 100644 --- a/app/lib/deploy/qiniu.php +++ b/app/lib/deploy/qiniu.php @@ -42,6 +42,8 @@ class qiniu implements DeployInterface $this->deploy_cdn($domain, $cert_id); } elseif ($config['product'] == 'oss') { $this->deploy_oss($domain, $cert_id); + } elseif ($config['product'] == 'pili') { + $this->deploy_pili($config['pili_hub'], $domain, $cert_name); } else { throw new Exception('未知的产品类型'); } @@ -87,6 +89,15 @@ class qiniu implements DeployInterface $this->log('OSS域名 ' . $domain . ' 证书部署成功!'); } + private function deploy_pili($hub, $domain, $cert_name) + { + $param = [ + 'CertName' => $cert_name, + ]; + $this->client->pili_request('POST', '/v2/hubs/'.$hub.'/domains/'.$domain.'/cert', null, $param); + $this->log('视频直播域名 ' . $domain . ' 证书部署成功!'); + } + private function get_cert_id($fullchain, $privatekey, $common_name, $cert_name) { $cert_id = null; diff --git a/app/lib/deploy/tencent.php b/app/lib/deploy/tencent.php index a3f7d82..c4bd892 100644 --- a/app/lib/deploy/tencent.php +++ b/app/lib/deploy/tencent.php @@ -37,7 +37,7 @@ class tencent implements DeployInterface if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); if (empty($config['cos_bucket'])) throw new Exception('存储桶名称不能为空'); if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); - $instance_id = $config['regionid'] . '#' . $config['cos_bucket'] . '#' . $config['domain']; + $instance_id = $config['regionid'] . '|' . $config['cos_bucket'] . '|' . $config['domain']; $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid'], $this->proxy); } elseif ($config['product'] == 'tke') { if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); @@ -52,6 +52,10 @@ class tencent implements DeployInterface if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); $instance_id = $config['regionid'] . '|' . $config['lighthouse_id'] . '|' . $config['domain']; $this->client = new TencentCloud($this->SecretId, $this->SecretKey, 'ssl.tencentcloudapi.com', 'ssl', '2019-12-05', $config['regionid'], $this->proxy); + } elseif ($config['product'] == 'ddos') { + if (empty($config['lighthouse_id'])) throw new Exception('实例ID不能为空'); + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $instance_id = $config['lighthouse_id'] . '|' . $config['domain'] . '|443'; } elseif ($config['product'] == 'clb') { return $this->deploy_clb($cert_id, $config); } elseif ($config['product'] == 'scf') { diff --git a/app/utils/CheckUtils.php b/app/utils/CheckUtils.php index 390b414..377be27 100644 --- a/app/utils/CheckUtils.php +++ b/app/utils/CheckUtils.php @@ -79,6 +79,9 @@ class CheckUtils $target = gethostbyname($target); if (!$target) return ['status' => false, 'errmsg' => 'DNS resolve failed', 'usetime' => 0]; } + if (filter_var($target, FILTER_VALIDATE_IP) && strpos($target, ':') !== false) { + $target = '['.$target.']'; + } $starttime = getMillisecond(); $fp = @fsockopen($target, $port, $errCode, $errStr, $timeout); if ($fp) { diff --git a/app/view/cert/cname.html b/app/view/cert/cname.html index e7867e6..cb6e003 100644 --- a/app/view/cert/cname.html +++ b/app/view/cert/cname.html @@ -4,6 +4,7 @@ @@ -120,7 +121,7 @@ $(document).ready(function(){ } else { html += '未验证'; } - html += ' '; + html += ''; return html; } }, diff --git a/config/app.php b/config/app.php index 3c2e640..01b7a0f 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' => '1026', + 'version' => '1027', 'dbversion' => '1023' ];