mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-02-21 15:31:12 +08:00
Merge branch 'main' of github.com:netcccyun/dnsmgr
This commit is contained in:
commit
8e7adead48
@ -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");
|
||||
|
||||
@ -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,
|
||||
],
|
||||
],
|
||||
|
||||
@ -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('<root/>');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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'] . ' 部署证书成功!');
|
||||
}
|
||||
|
||||
|
||||
@ -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('<?xml version="1.0" encoding="utf-8"?><DistributionConfig></DistributionConfig>');
|
||||
$data['ViewerCertificate']['CloudFrontDefaultCertificate'] = 'false';
|
||||
unset($data['ViewerCertificate']['Certificate']);
|
||||
unset($data['ViewerCertificate']['CertificateSource']);
|
||||
|
||||
$xml = new \SimpleXMLElement('<DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/"></DistributionConfig>');
|
||||
$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;
|
||||
}
|
||||
|
||||
|
||||
471
app/lib/deploy/wangsu.php
Normal file
471
app/lib/deploy/wangsu.php
Normal file
@ -0,0 +1,471 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\deploy;
|
||||
|
||||
use app\lib\DeployInterface;
|
||||
use Exception;
|
||||
|
||||
class wangsu implements DeployInterface
|
||||
{
|
||||
private $logger;
|
||||
private $username;
|
||||
private $apiKey;
|
||||
private $spKey;
|
||||
private $proxy;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
public/static/images/wangsu.ico
Normal file
BIN
public/static/images/wangsu.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Loading…
Reference in New Issue
Block a user