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'
];