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