diff --git a/app/common.php b/app/common.php
index f654125..3815d78 100644
--- a/app/common.php
+++ b/app/common.php
@@ -392,7 +392,7 @@ function clearDirectory($dir): bool
return true;
}
-function curl_client($url, $data = null, $referer = null, $cookie = null, $headers = null, $proxy = false, $method = null, $timeout = 5)
+function curl_client($url, $data = null, $referer = null, $cookie = null, $headers = null, $proxy = false, $method = null, $timeout = 5, $default_headers = true)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
@@ -400,11 +400,15 @@ function curl_client($url, $data = null, $referer = null, $cookie = null, $heade
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
- $httpheader[] = "Accept: */*";
- $httpheader[] = "Accept-Language: zh-CN,zh;q=0.8";
- $httpheader[] = "Connection: close";
- if ($headers) {
- $httpheader = array_merge($httpheader, $headers);
+ if ($default_headers === true) {
+ $httpheader[] = "Accept: */*";
+ $httpheader[] = "Accept-Language: zh-CN,zh;q=0.8";
+ $httpheader[] = "Connection: close";
+ if ($headers) {
+ $httpheader = array_merge($headers, $httpheader);
+ }
+ } else {
+ $httpheader = $headers;
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36");
diff --git a/app/lib/DeployHelper.php b/app/lib/DeployHelper.php
index 569eb37..3abb493 100644
--- a/app/lib/DeployHelper.php
+++ b/app/lib/DeployHelper.php
@@ -1210,6 +1210,74 @@ class DeployHelper
],
],
],
+ 'wangsu' => [
+ 'name' => '网宿科技',
+ 'class' => 2,
+ 'icon' => 'wangsu.ico',
+ 'note' => '适用产品:网页加速、下载分发、全站加速、点播分发、直播分发、上传加速、移动加速、上网加速、S-P2P、PCDN、应用性能管理、WEB应用防火墙、BotGuard爬虫管理、WSS、DMS、DDoS云清洗、应用加速、应用安全加速解决方案、IPv6一体化解决方案、电商安全加速解决方案、金融安全加速解决方案、政企安全加速解决方案、DDoS云清洗(非网站业务)、区块链安全加速解决方案、IPv6安全加速解决方案、CDN Pro。暂不支持AKSK鉴权。',
+ 'inputs' => [
+ 'username' => [
+ 'name' => '账号',
+ 'type' => 'input',
+ 'placeholder' => '',
+ 'required' => true,
+ ],
+ 'apiKey' => [
+ 'name' => 'APIKEY',
+ 'type' => 'input',
+ 'placeholder' => '自行联系提供商申请',
+ 'required' => true,
+ ],
+ 'spKey' => [
+ 'name' => '特殊KEY',
+ 'type' => 'input',
+ 'placeholder' => '特殊场景下才需要使用的APIKEY,留空默认同APIKEY',
+ ],
+ 'proxy' => [
+ 'name' => '使用代理服务器',
+ 'type' => 'radio',
+ 'options' => [
+ '0' => '否',
+ '1' => '是',
+ ],
+ 'value' => '0'
+ ],
+ ],
+ 'taskinputs' => [
+ 'product' => [
+ 'name' => '要部署的产品',
+ 'type' => 'select',
+ 'options' => [
+ ['value'=>'cdn', 'label'=>'CDN'],
+ ['value'=>'cdnpro', 'label'=>'CDN Pro'],
+ ['value'=>'certificate', 'label'=>'证书管理']
+ ],
+ 'value' => 'cdn',
+ 'required' => true,
+ ],
+ 'domains' => [
+ 'name' => '绑定的域名',
+ 'type' => 'input',
+ 'show' => 'product==\'cdn\'',
+ 'placeholder' => '多个域名可使用,分隔',
+ 'required' => true,
+ ],
+ 'domain' => [
+ 'name' => '绑定的域名',
+ 'type' => 'input',
+ 'show' => 'product==\'cdnpro\'',
+ 'placeholder' => '不支持输入多个域名',
+ 'required' => true,
+ ],
+ 'cert_id' => [
+ 'name' => '证书ID',
+ 'type' => 'input',
+ 'show' => 'product==\'certificate\'',
+ 'placeholder' => '',
+ 'required' => true,
+ ],
+ ],
+ ],
'baishan' => [
'name' => '白山云',
'class' => 2,
@@ -1371,7 +1439,7 @@ class DeployHelper
'name' => 'AWS',
'class' => 2,
'icon' => 'aws.ico',
- 'note' => '支持部署到Amazon CloudFront',
+ 'note' => '支持部署到Amazon CloudFront、AWS Certificate Manager',
'inputs' => [
'AccessKeyId' => [
'name' => 'AccessKeyId',
@@ -1401,14 +1469,24 @@ class DeployHelper
'type' => 'select',
'options' => [
['value'=>'cloudfront', 'label'=>'CloudFront'],
+ ['value'=>'acm', 'label'=>'AWS Certificate Manager'],
],
- 'value' => 'cloudfront',
+ 'value' => 'acm',
'required' => true,
],
'distribution_id' => [
'name' => '分配ID',
'type' => 'input',
'placeholder' => 'distributions id',
+ 'show' => 'product==\'cloudfront\'',
+ 'required' => true,
+ ],
+ 'acm_arn' => [
+ 'name' => 'ACM ARN',
+ 'type' => 'input',
+ 'placeholder' => '',
+ 'show' => 'product==\'acm\'',
+ 'note' => '在AWS Certificate Manager控制台查看证书的ARN',
'required' => true,
],
],
diff --git a/app/lib/client/AWS.php b/app/lib/client/AWS.php
index a67e1f6..36b841d 100644
--- a/app/lib/client/AWS.php
+++ b/app/lib/client/AWS.php
@@ -145,6 +145,7 @@ class AWS
$path = '/' . $this->version . $path;
$body = '';
+ $query = [];
if ($method == 'GET' || $method == 'DELETE') {
$query = $params;
} else {
@@ -327,16 +328,31 @@ class AWS
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
}
- private function array2xml($array, $xml = null)
+ private function array2xml($array, $xml = null, $parentTagName = 'root')
{
if ($xml === null) {
$xml = new \SimpleXMLElement('');
}
foreach ($array as $key => $value) {
+ // 确定当前标签名:如果是数字键名,使用父级标签名,否则使用当前键名
+ $tagName = is_numeric($key) ? $parentTagName : $key;
+
if (is_array($value)) {
- $subNode = $xml->addChild($key);
- $this->array2xml($value, $subNode);
+ // 检查数组的第一个子节点的键是否为0
+ $firstKey = array_key_first($value);
+ $isFirstKeyZero = ($firstKey === 0 || $firstKey === '0');
+
+ if ($isFirstKeyZero) {
+ // 如果第一个子节点的键是0,则不生成当前节点标签,直接递归子节点
+ $this->array2xml($value, $xml, $tagName);
+
+ } else {
+ // 否则生成当前节点标签,并递归子节点
+ $subNode = $xml->addChild($tagName);
+ $this->array2xml($value, $subNode, $tagName);
+ }
+
} else {
$xml->addChild($key, $value);
}
diff --git a/app/lib/deploy/aliyun.php b/app/lib/deploy/aliyun.php
index 4a255e4..7e361b9 100644
--- a/app/lib/deploy/aliyun.php
+++ b/app/lib/deploy/aliyun.php
@@ -236,7 +236,7 @@ class aliyun implements DeployInterface
if (empty($config['oss_endpoint'])) throw new Exception('OSS Endpoint不能为空');
if (empty($config['oss_bucket'])) throw new Exception('OSS Bucket不能为空');
$client = new AliyunOSS($this->AccessKeyId, $this->AccessKeySecret, $config['oss_endpoint']);
- $client->addBucketCnameCert($config['oss_bucket'], $config['domain'], $cert_id);
+ $client->addBucketCnameCert($config['oss_bucket'], $config['domain'], $cert_id . '-cn-hangzhou');
$this->log('OSS域名 ' . $config['domain'] . ' 部署证书成功!');
}
diff --git a/app/lib/deploy/aws.php b/app/lib/deploy/aws.php
index 74773ff..9470c25 100644
--- a/app/lib/deploy/aws.php
+++ b/app/lib/deploy/aws.php
@@ -29,22 +29,24 @@ class aws implements DeployInterface
}
public function deploy($fullchain, $privatekey, $config, &$info)
+ {
+ if ($config['product'] == 'acm') {
+ if (empty($config['acm_arn'])) throw new Exception('ACM ARN不能为空');
+ $this->get_cert_id($fullchain, $privatekey, $config['acm_arn'], true);
+ } else {
+ $this->deploy_cloudfront($fullchain, $privatekey, $config, $info);
+ }
+ }
+
+ private function deploy_cloudfront($fullchain, $privatekey, $config, &$info)
{
if (empty($config['distribution_id'])) throw new Exception('分配ID不能为空');
$certInfo = openssl_x509_parse($fullchain, true);
if (!$certInfo) throw new Exception('证书解析失败');
- $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
- if (isset($info['cert_id']) && isset($info['cert_name']) && $info['cert_name'] == $config['cert_name']) {
- $cert_id = $info['cert_id'];
- $this->log('证书已上传:' . $cert_id);
- } else {
- $cert_id = $this->get_cert_id($fullchain, $privatekey);
- $this->log('证书上传成功:' . $cert_id);
- $info['cert_id'] = $cert_id;
- $info['cert_name'] = $config['cert_name'];
- usleep(500000);
- }
+ $cert_id = isset($info['cert_id']) ? $info['cert_id'] : null;
+ $cert_id = $this->get_cert_id($fullchain, $privatekey, $cert_id);
+ usleep(500000);
$client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'cloudfront.amazonaws.com', 'cloudfront', '2020-05-31', 'us-east-1', $this->proxy);
try {
@@ -54,20 +56,71 @@ class aws implements DeployInterface
}
$data['ViewerCertificate']['ACMCertificateArn'] = $cert_id;
- $data['ViewerCertificate']['CloudFrontDefaultCertificate'] = false;
- $xml = new \SimpleXMLElement('');
+ $data['ViewerCertificate']['CloudFrontDefaultCertificate'] = 'false';
+ unset($data['ViewerCertificate']['Certificate']);
+ unset($data['ViewerCertificate']['CertificateSource']);
+
+ $xml = new \SimpleXMLElement('');
$client->requestXmlN('PUT', '/distribution/' . $config['distribution_id'] . '/config', $data, $xml);
$this->log('分配ID: ' . $config['distribution_id'] . ' 证书部署成功!');
}
- private function get_cert_id($fullchain, $privatekey)
+ private function get_cert_id($fullchain, $privatekey, $cert_id = null, $acm = false)
{
- $cert = explode('-----END CERTIFICATE-----', $fullchain)[0] . '-----END CERTIFICATE-----';
+ if ($acm === true && $cert_id == null) {
+ throw new Exception('ACM ARN不能为空');
+ }
+
+ $certificates = explode('-----END CERTIFICATE-----', $fullchain);
+ $cert = $certificates[0] . '-----END CERTIFICATE-----';
+
+ $client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'acm.us-east-1.amazonaws.com', 'acm', '', 'us-east-1', $this->proxy);
+
+ if (!empty($cert_id)) {
+ try {
+ $data = $client->request('POST', 'CertificateManager.GetCertificate', [
+ 'CertificateArn' => $cert_id
+ ]);
+ // 如果成功获取证书信息,说明证书存在,直接返回cert_id
+ if (isset($data['Certificate']) && trim($data['Certificate']) == trim($cert)) {
+ $this->log('证书已是最新,ACM ARN:' . $cert_id);
+ return $cert_id;
+ } else {
+ $this->log('证书已过期或被删除,准备更新或者重新上传');
+ }
+ } catch (Exception $e) {
+ if ($acm === true) {
+ throw new Exception('获取证书信息失败,请检查ACM ARN是否正确:' . $e->getMessage());
+ }
+ $this->log('证书已被删除:' . $cert_id. ',准备重新上传');
+ }
+ }
+
+ $certificateChain = '';
+ if (count($certificates) > 1) {
+ // 从第二个证书开始,重新拼接中间证书链
+ for ($i = 1; $i < count($certificates); $i++) {
+ if (trim($certificates[$i]) !== '') { // 忽略空字符串(可能由末尾分割产生)
+ $certificateChain .= $certificates[$i] . '-----END CERTIFICATE-----';
+ }
+ }
+ }
+
$param = [
'Certificate' => base64_encode($cert),
'PrivateKey' => base64_encode($privatekey),
];
+ // 如果有中间证书链,则添加到参数中
+ if (!empty($certificateChain)) {
+ $param['CertificateChain'] = base64_encode($certificateChain);
+ }
+
+ // 如果是ACM,则添加ARN参数,用于更新证书
+ if ($acm === true) {
+ $param['CertificateArn'] = $cert_id;
+ }
+
$client = new AWSClient($this->AccessKeyId, $this->SecretAccessKey, 'acm.us-east-1.amazonaws.com', 'acm', '', 'us-east-1', $this->proxy);
try {
$data = $client->request('POST', 'CertificateManager.ImportCertificate', $param);
@@ -75,6 +128,11 @@ class aws implements DeployInterface
} catch (Exception $e) {
throw new Exception('上传证书失败:' . $e->getMessage());
}
+
+ $this->log('证书上传成功:' . $cert_id);
+
+ $info['cert_id'] = $cert_id;
+
return $cert_id;
}
diff --git a/app/lib/deploy/wangsu.php b/app/lib/deploy/wangsu.php
new file mode 100644
index 0000000..fa7e2b0
--- /dev/null
+++ b/app/lib/deploy/wangsu.php
@@ -0,0 +1,471 @@
+username = $config['username'];
+ $this->apiKey = $config['apiKey'];
+ $this->spKey = $config['spKey'];
+ $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
+ }
+
+ public function check()
+ {
+ if (empty($this->username) || empty($this->apiKey)) throw new Exception('必填参数不能为空');
+ $this->request('/cdn/certificates');
+ return true;
+ }
+
+ public function deploy($fullchain, $privatekey, $config, &$info)
+ {
+ if ($config['product'] == 'cdnpro') {
+ $this->deploy_cdnpro($fullchain, $privatekey, $config, $info);
+
+ } elseif ($config['product'] == 'cdn') {
+ $this->deploy_cdn($fullchain, $privatekey, $config, $info);
+
+ } elseif ($config['product'] == 'certificate') {
+ $certInfo = openssl_x509_parse($fullchain, true);
+ if (!$certInfo) {
+ throw new Exception('证书解析失败');
+ }
+ $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
+ $serial_no = strtolower($certInfo['serialNumberHex']);
+ $this->get_cert_id($fullchain, $privatekey, $cert_name, $config['cert_id'], $serial_no, true);
+ } else {
+ throw new Exception('未知的产品类型');
+ }
+ }
+
+ public function deploy_cdn($fullchain, $privatekey, $config, &$info)
+ {
+ if (empty($config['domains'])) {
+ throw new Exception('绑定的域名不能为空');
+ }
+ $domains = explode(',', $config['domains']);
+
+ $certInfo = openssl_x509_parse($fullchain, true);
+ if (!$certInfo) {
+ throw new Exception('证书解析失败');
+ }
+
+ $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
+ $serial_no = strtolower($certInfo['serialNumberHex']);
+ $this->log('证书序列号:' . $serial_no);
+ $cert_id = isset($info['cert_id']) ? $info['cert_id'] : null;
+ $cert_id = $this->get_cert_id($fullchain, $privatekey, $cert_name, $cert_id, $serial_no, false);
+
+ $param = [
+ 'certificateId' => $cert_id,
+ 'domainNames' => $domains
+ ];
+
+ try {
+ $data = $this->request('/api/config/certificate/batch', $param, true, null, 'PUT');
+ } catch (Exception $e) {
+ throw new Exception('绑定域名失败:' . $e->getMessage());
+ }
+
+ $this->log('绑定证书成功,证书ID:' . $cert_id);
+ $info['cert_id'] = $cert_id;
+ }
+
+ public function deploy_cdnpro($fullchain, $privatekey, $config, &$info)
+ {
+ if (empty($config['domain'])) {
+ throw new Exception('绑定的域名不能为空');
+ }
+ $domain = $config['domain'];
+
+ $certInfo = openssl_x509_parse($fullchain, true);
+ if (!$certInfo) {
+ throw new Exception('证书解析失败');
+ }
+
+ $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t'];
+ $cert_id = $this->get_cert_id_cdnpro($fullchain, $privatekey, $cert_name);
+
+ try {
+ $hostnameInfo = $this->request('/cdn/hostnames/' . $domain);
+ } catch (Exception $e) {
+ throw new Exception('获取域名信息失败:' . $e->getMessage());
+ }
+
+ if (empty($hostnameInfo["propertyInProduction"])) {
+ throw new Exception('域名 ' . $domain . ' 不存在或未部署到生产环境');
+ } else {
+ $this->log('CDN域名 ' . $domain . ' 对应的加速项目ID:' . $hostnameInfo["propertyInProduction"]["propertyId"]);
+ $this->log('CDN域名 ' . $domain . ' 对应的加速项目生产版本:' . $hostnameInfo["propertyInProduction"]["version"]);
+ }
+
+ if ($hostnameInfo["propertyInProduction"]["certificateId"] == $cert_id) {
+ $this->log('CDN域名 ' . $domain . ' 已绑定证书:' . $cert_name);
+ return;
+ }
+
+ try {
+ $properity = $this->request('/cdn/properties/' . $hostnameInfo["propertyInProduction"]["propertyId"] . '/versions/' . $hostnameInfo["propertyInProduction"]["version"]);
+ } catch (Exception $e) {
+ throw new Exception('获取加速项目版本信息失败:' . $e->getMessage());
+ }
+
+ $properityConfig = $properity["configs"];
+ $properityConfig["tlsCertificateId"] = $cert_id;
+
+ try {
+ $data = $this->request('/cdn/properties/' . $hostnameInfo["propertyInProduction"]["propertyId"] . '/versions', $properityConfig, true);
+ } catch (Exception $e) {
+ throw new Exception('新增加速项目版本失败:' . $e->getMessage());
+ }
+
+ $url_parts = parse_url($data);
+ $path_parts = explode('/', $url_parts['path']);
+ $newVersion = end($path_parts);
+
+ $param = [
+ 'propertyId' => $hostnameInfo["propertyInProduction"]["propertyId"],
+ 'version' => intval($newVersion),
+ ];
+
+ try {
+ $data = $this->request('/cdn/validations', $param, true);
+ } catch (Exception $e) {
+ throw new Exception('发起加速项目验证失败:' . $e->getMessage());
+ }
+
+ $url_parts = parse_url($data);
+ $path_parts = explode('/', $url_parts['path']);
+ $validationTaskId = end($path_parts);
+ $this->log('验证任务ID:' . $validationTaskId);
+
+ $attempts = 0;
+ $maxAttempts = 12;
+ $status = null;
+
+ do {
+ sleep(5);
+
+ try {
+ $data = $this->request('/cdn/validations/' . $validationTaskId);
+ } catch (Exception $e) {
+ throw new Exception('获取验证任务状态失败:' . $e->getMessage());
+ }
+
+ $status = $data['status'];
+
+ if ($status === 'failed') {
+ throw new Exception('证书绑定失败,加速项目验证失败');
+ }
+
+ if ($status === 'succeeded') {
+ break; // 验证成功立即退出循环
+ }
+
+ $attempts++;
+ } while ($attempts < $maxAttempts);
+
+ if ($status !== 'succeeded') {
+ throw new Exception('证书绑定超时,加速项目验证时间过长');
+ }
+
+ $this->log('加速项目验证成功,开始部署...');
+
+ $deploymentTasks = [
+ 'target' => 'production',
+ 'actions' => [
+ [
+ 'action' => 'deploy_cert',
+ 'certificateId' => $cert_id,
+ 'version' => 1,
+ ],
+ [
+ 'action' => 'deploy_property',
+ 'propertyId' => $hostnameInfo["propertyInProduction"]["propertyId"],
+ 'version' => intval($newVersion),
+ ]
+ ],
+ 'name' => 'Deploy certificate and property for ' . $hostnameInfo["propertyInProduction"]["propertyId"],
+ ];
+
+ try {
+ $data = $this->request('/cdn/deploymentTasks', $deploymentTasks, true, null, 'POST', false, ['Check-Certificate' => 'no', 'Check-Usage' => 'no']);
+ } catch (Exception $e) {
+ throw new Exception('下发证书部署任务失败:' . $e->getMessage());
+ }
+
+ $url_parts = parse_url($data);
+ $path_parts = explode('/', $url_parts['path']);
+ $deploymentTaskId = end($path_parts);
+
+ $this->log('CDN域名 ' . $domain . ' 绑定证书部署任务下发成功,部署任务ID:' . $deploymentTaskId);
+ $info['cert_id'] = $cert_id;
+ }
+
+ private function get_cert_id($fullchain, $privatekey, $cert_name, $cert_id = null, $serial_no = null, $overwrite = false)
+ {
+ if ($cert_id) {
+ try {
+ $data = $this->request('/api/certificate/' . $cert_id);
+ } catch (Exception $e) {
+ throw new Exception('获取证书详情失败:' . $e->getMessage());
+ }
+
+ if (isset($data['message']) && $data['message'] == 'success' && $data['data']['name'] == $cert_name && $data['data']['serial'] == $serial_no) {
+ $this->log('证书已是最新,证书ID:' . $cert_id);
+ return $cert_id;
+ }
+
+ $this->log('证书已过期或被删除,准备重新上传');
+
+ } elseif ($overwrite === true) {
+ throw new Exception('证书ID不能为空');
+ }
+
+ if ($overwrite === true) {
+ $param = [
+ 'name' => $cert_name,
+ 'certificate' => $fullchain,
+ 'privateKey' => $privatekey,
+ ];
+
+ try {
+ $data = $this->request('/api/certificate/' . $cert_id, $param, true, null, 'PUT');
+ $this->log('更新证书成功,证书ID:' . $cert_id);
+ return $cert_id;
+ } catch (Exception $e) {
+ throw new Exception('更新证书失败:' . $e->getMessage());
+ }
+ }
+
+ try {
+ $data = $this->request('/api/ssl/certificate');
+ } catch (Exception $e) {
+ throw new Exception('获取证书列表失败:' . $e->getMessage());
+ }
+
+ $certificates = $data['ssl-certificate'];
+
+ if (!empty($certificates)) {
+ foreach ($certificates as $cert) {
+ if ($serial_no == $cert['certificate-serial']) {
+ $cert_id = $cert['certificate-id'];
+ $this->log('证书' . $cert_name . '已存在,新证书ID:' . $cert_id);
+ try {
+ $this->request('/api/certificate/' . $cert_id, ['name' => $cert_name], true, null, 'PUT');
+ } catch (Exception $e) {
+ throw new Exception('证书更名失败:' . $e->getMessage());
+ }
+ $this->log('将证书ID为' . $cert_id . '的证书更名为:' . $cert_name);
+ return $cert_id;
+
+ } elseif ($cert_name == $cert['name']) {
+ $this->log('证书' . $cert_name . '已存在,但序列号(' . $cert['certificate-id'] . ')不匹配,准备重新上传');
+ try {
+ $this->request('/api/certificate/' . $cert['certificate-id'], [['name'] => $cert_name . '-bak'], true, null, 'PUT');
+ } catch (Exception $e) {
+ throw new Exception('证书更名失败:' . $e->getMessage());
+ }
+ $this->log('将证书ID为' . $cert['certificate-id'] . '的证书更名为:' . $cert_name . '-bak');
+ }
+ }
+ }
+
+ $param = [
+ 'name' => $cert_name,
+ 'certificate' => $fullchain,
+ 'privateKey' => $privatekey,
+ ];
+
+ try {
+ $data = $this->request('/api/certificate', $param, true, null, 'POST', true);
+ } catch (Exception $e) {
+ throw new Exception('上传证书失败:' . $e->getMessage());
+ }
+
+ $url_parts = parse_url($data);
+ $path_parts = explode('/', $url_parts['path']);
+ $cert_id = end($path_parts);
+ $this->log('上传证书成功,证书ID:' . $cert_id);
+
+ return $cert_id;
+ }
+
+ private function get_cert_id_cdnpro($fullchain, $privatekey, $cert_name)
+ {
+ $cert_id = null;
+
+ try {
+ $data = $this->request('/cdn/certificates?search=' . urlencode($cert_name));
+ } catch (Exception $e) {
+ throw new Exception('获取证书列表失败:' . $e->getMessage());
+ }
+
+ if ($data['count'] > 0) {
+ foreach ($data['certificates'] as $cert) {
+ if ($cert_name == $cert['name']) {
+ $cert_id = $cert['certificateId'];
+ $this->log('证书' . $cert_name . '已存在,证书ID:' . $cert_id);
+ return $cert_id;
+ }
+ }
+ }
+
+ $date = gmdate("D, d M Y H:i:s T");
+ $encryptedKey = $this->encryptPrivateKey($privatekey, $date);
+ $param = [
+ 'name' => $cert_name,
+ 'autoRenew' => 'Off',
+ 'newVersion' => [
+ 'privateKey' => $encryptedKey,
+ 'certificate' => $fullchain,
+ ]
+ ];
+
+ try {
+ $data = $this->request('/cdn/certificates', $param, true, $date);
+ } catch (Exception $e) {
+ throw new Exception('上传证书失败:' . $e->getMessage());
+ }
+
+ $url_parts = parse_url($data);
+ $path_parts = explode('/', $url_parts['path']);
+ $cert_id = end($path_parts);
+ $this->log('上传证书成功,证书ID:' . $cert_id);
+
+ usleep(500000);
+
+ return $cert_id;
+ }
+
+ private function encryptPrivateKey($privateKey, $date = null)
+ {
+ // 获取当前 GMT 时间(DATE)
+ if (empty($date)) {
+ $date = gmdate("D, d M Y H:i:s T");
+ }
+
+ // 生成 HMAC-SHA256 密钥材料
+ if (!empty($this->spKey)) {
+ $apiKey = $this->spKey;
+ } else {
+ $apiKey = $this->apiKey;
+ }
+ $hmac = hash_hmac('sha256', $date, $apiKey, true);
+ $aesIvKeyHex = bin2hex($hmac);
+
+ if (strlen($aesIvKeyHex) != 64) {
+ throw new Exception("Invalid HMAC length: " . strlen($aesIvKeyHex));
+ }
+
+ // 提取 IV 和 Key
+ $ivHex = substr($aesIvKeyHex, 0, 32);
+ $keyHex = substr($aesIvKeyHex, 32, 64);
+
+ $iv = hex2bin($ivHex);
+ $key = hex2bin($keyHex);
+
+ $blockSize = 16; // AES 块大小为 16 字节
+ $plainLen = strlen($privateKey);
+ $padLen = $blockSize - ($plainLen % $blockSize);
+ $padding = str_repeat(chr($padLen), $padLen);
+ $plainText = $privateKey . $padding;
+
+ // AES-128-CBC 加密
+ $encrypted = openssl_encrypt(
+ $plainText,
+ 'AES-128-CBC',
+ $key,
+ OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
+ $iv
+ );
+
+ if ($encrypted === false) {
+ throw new Exception("Encryption failed: " . openssl_error_string());
+ }
+
+ // 返回 Base64 编码结果
+ return base64_encode($encrypted);
+ }
+
+ private function request($path, $data = null, $json = false, $date = null, $method = null, $getLocation = false, $headers = [])
+ {
+ $body = null;
+ if ($data) {
+ $body = $json ? json_encode($data) : http_build_query($data);
+ }
+
+ if (empty($date)) {
+ $date = gmdate("D, d M Y H:i:s T");
+ }
+
+ $hmac = hash_hmac('sha1', $date, $this->apiKey, true);
+ $signature = base64_encode($hmac);
+ $authorization = 'Basic ' . base64_encode($this->username . ':' . $signature);
+
+ if (empty($headers)) {
+ $headers = [
+ 'Authorization: ' . $authorization,
+ 'Date: ' . $date,
+ 'Accept: application/json',
+ 'Connection: close',
+ ];
+ } else {
+ $headers[] = 'Authorization: ' . $authorization;
+ $headers[] = 'Date: ' . $date;
+ $headers[] = 'Accept: application/json';
+ $headers[] = 'Connection: close';
+ }
+
+ if ($body && $json) {
+ $headers[] = 'Content-Type: application/json';
+ }
+
+ $url = 'https://open.chinanetcenter.com' . $path;
+ $response = curl_client($url, $body, null, null, $headers, $this->proxy, $method, 30, false);
+ $result = json_decode($response['body'], true);
+
+ if ((isset($response['code']) && $response['code'] == 201) || (isset($response['code']) && $response['code'] == 200 && $getLocation === true)) {
+ if (preg_match('/Location:\s*(.*)/i', $response['header'], $matches)) {
+ $location = trim($matches[1]); // 提取 Location 头部的值并去除多余空格
+ if (!empty($location)) {
+ return $location;
+ }
+ }
+ // 如果没有找到 Location 头部,返回默认值 true
+ return true;
+
+ } elseif (isset($response['code']) && $response['code'] >= 200 && $response['code'] <= 299) {
+ return isset($result) ? $result : true;
+
+ } elseif (isset($result['message'])) {
+ throw new Exception($result['message']);
+
+ } else {
+ throw new Exception('请求失败');
+ }
+ }
+
+ public function setLogger($func)
+ {
+ $this->logger = $func;
+ }
+
+ private function log($txt)
+ {
+ if ($this->logger) {
+ call_user_func($this->logger, $txt);
+ }
+ }
+}
diff --git a/public/static/images/wangsu.ico b/public/static/images/wangsu.ico
new file mode 100644
index 0000000..7f1b01f
Binary files /dev/null and b/public/static/images/wangsu.ico differ