refactor(dnspod): 迁移DNSPod适配到腾讯云API 3.0版本
2 个文件变更
+134
-97
13172193298@163.com
| •app_plugins/domain_shop/admin/dns_provider.php | +7 -6 |
| •app_plugins/domain_shop/lib/provider/dnspod.php | +127 -91 |
变更内容
diff --git a/app_plugins/domain_shop/admin/dns_provider.php b/app_plugins/domain_shop/admin/dns_provider.php
index 6990de8..a5a8aeb 100644
--- a/app_plugins/domain_shop/admin/dns_provider.php
+++ b/app_plugins/domain_shop/admin/dns_provider.php
@@ -28,13 +28,14 @@ $providers = $DB->get_all_prepare("SELECT * FROM plg_dns_provider order by id as
<input type="text" class="form-control" id="name" placeholder="如:我的 DNSPod 账号">
</div>
<div class="form-group">
- <label>API Token ID</label>
- <input type="text" class="form-control" id="api_id" placeholder="DNSPod API Token ID">
- <small>在 DNSPod 控制台 → 安全设置 → API Token 中创建</small>
+ <label>SecretId(腾讯云 API 密钥 ID)</label>
+ <input type="text" class="form-control" id="api_id" placeholder="形如 AKIDxxxxxxxxxxxxxxxx">
+ <small>腾讯云控制台 → 访问管理 → API 密钥管理 中创建,形如 AKID 开头</small>
</div>
<div class="form-group">
- <label>API Token Secret</label>
- <input type="text" class="form-control" id="api_secret" placeholder="DNSPod API Token Secret">
+ <label>SecretKey(腾讯云 API 密钥 Key)</label>
+ <input type="text" class="form-control" id="api_secret" placeholder="腾讯云 SecretKey">
+ <small>DNSPod 已停用旧版 API Token,请改用腾讯云 API 3.0 密钥(SecretId/SecretKey)</small>
</div>
<div class="form-group">
<label class="btn-block">是否启用</label>
@@ -62,7 +63,7 @@ $providers = $DB->get_all_prepare("SELECT * FROM plg_dns_provider order by id as
<table class="table table-bordered table-striped">
<thead>
<tr>
- <th>ID</th><th>类型</th><th>名称</th><th>Token ID</th>
+ <th>ID</th><th>类型</th><th>名称</th><th>SecretId</th>
<th>状态</th><th>添加时间</th><th>操作</th>
</tr>
</thead>
diff --git a/app_plugins/domain_shop/lib/provider/dnspod.php b/app_plugins/domain_shop/lib/provider/dnspod.php
index 65880ef..13bb717 100644
--- a/app_plugins/domain_shop/lib/provider/dnspod.php
+++ b/app_plugins/domain_shop/lib/provider/dnspod.php
@@ -1,18 +1,22 @@
<?php
/**
- * domain_shop 插件 - DNSPod API 适配器
- * 文档:https://cloud.tencent.com/document/product/1427
+ * domain_shop 插件 - DNSPod(腾讯云 API 3.0)适配器
+ * 文档:https://cloud.tencent.com/document/api/1427
*
- * 鉴权:API Token(ID + Secret),HTTP Header:LoginToken
- * 签名:无签名要求,Token 直接传递
+ * 旧版 DNSPod API(dnsapi.cn + login_token)已停止维护,此处迁移至腾讯云 API 3.0:
+ * - 接口域名:dnspod.tencentcloudapi.com,Version=2021-03-23
+ * - 鉴权:腾讯云 API 密钥(SecretId + SecretKey),TC3-HMAC-SHA256 签名
+ * - 请求:POST + application/json
*/
if (!defined('IN_CRONLITE')) exit;
class DomainShop_DNSPod
{
- private $apiId;
- private $apiSecret;
- private $endpoint = 'https://dnsapi.cn/';
+ private $apiId; // 腾讯云 SecretId
+ private $apiSecret; // 腾讯云 SecretKey
+ private $endpoint = 'https://dnspod.tencentcloudapi.com';
+ private $service = 'dnspod';
+ private $version = '2021-03-23';
private $lastErr = '';
public function __construct($apiId, $apiSecret)
@@ -32,36 +36,41 @@ class DomainShop_DNSPod
*/
public function listDomains()
{
- $res = $this->call('Domain.List', []);
- if (empty($res['ok'])) return [];
- $body = $res['body'];
- $arr = json_decode($body, true);
- if (!is_array($arr) || ($arr['status']['code'] ?? '0') !== '1') {
- $this->lastErr = $arr['status']['message'] ?? 'Domain.List 失败';
- return [];
- }
- $domains = $arr['domains'] ?? [];
+ $resp = $this->call('DescribeDomainList', ['Type' => 'ALL', 'Limit' => 1000]);
+ if ($resp === false) return [];
$out = [];
- foreach ($domains as $d) {
- $out[] = ['id' => (string)$d['id'], 'name' => $d['name'] ?? ''];
+ foreach (($resp['DomainList'] ?? []) as $d) {
+ $out[] = ['id' => (string)($d['DomainId'] ?? ''), 'name' => $d['Name'] ?? ''];
}
return $out;
}
/**
- * 取某域名下记录列表
+ * 取某域名下记录列表(字段映射为小写风格,与旧适配器输出保持一致)
*/
public function listRecords($domain)
{
- $res = $this->call('Record.List', ['domain' => $domain]);
- if (empty($res['ok'])) return [];
- $body = $res['body'];
- $arr = json_decode($body, true);
- if (!is_array($arr) || ($arr['status']['code'] ?? '0') !== '1') {
- $this->lastErr = $arr['status']['message'] ?? 'Record.List 失败';
- return [];
+ $resp = $this->call('DescribeRecordList', [
+ 'Domain' => $domain,
+ 'Offset' => 0,
+ 'Limit' => 1000,
+ 'ErrorOnEmpty' => 'no',
+ ]);
+ if ($resp === false) return [];
+ $out = [];
+ foreach (($resp['RecordList'] ?? []) as $r) {
+ $out[] = [
+ 'id' => (string)($r['RecordId'] ?? ''),
+ 'name' => $r['Name'] ?? '',
+ 'type' => $r['Type'] ?? '',
+ 'value' => $r['Value'] ?? '',
+ 'line' => $r['Line'] ?? '',
+ 'ttl' => $r['TTL'] ?? 0,
+ 'mx' => $r['MX'] ?? 0,
+ 'status' => $r['Status'] ?? '',
+ ];
}
- return $arr['records'] ?? [];
+ return $out;
}
/**
@@ -71,26 +80,18 @@ class DomainShop_DNSPod
public function createRecord($domain, $name, $type, $value, $ttl = 600, $mx = 0)
{
$params = [
- 'domain' => $domain,
- 'sub_domain' => $name,
- 'record_type' => strtoupper($type),
- 'record_line' => '默认',
- 'value' => $value,
- 'ttl' => (int)$ttl,
+ 'Domain' => $domain,
+ 'SubDomain' => $name,
+ 'RecordType' => strtoupper($type),
+ 'RecordLine' => '默认',
+ 'Value' => $value,
+ 'TTL' => (int)$ttl,
];
- if (strtoupper($type) === 'MX') $params['mx'] = (int)$mx;
+ if (strtoupper($type) === 'MX') $params['MX'] = (int)$mx;
- $res = $this->call('Record.Create', $params);
- if (empty($res['ok'])) {
- $this->lastErr = 'HTTP 请求失败:' . ($res['error'] ?? '');
- return false;
- }
- $arr = json_decode($res['body'], true);
- if (!is_array($arr) || ($arr['status']['code'] ?? '0') !== '1') {
- $this->lastErr = $arr['status']['message'] ?? 'Record.Create 失败';
- return false;
- }
- return (string)($arr['record']['id'] ?? '');
+ $resp = $this->call('CreateRecord', $params);
+ if ($resp === false) return false;
+ return (string)($resp['RecordId'] ?? '');
}
/**
@@ -99,24 +100,18 @@ class DomainShop_DNSPod
public function updateRecord($domain, $recordId, $name, $type, $value, $ttl = 600, $mx = 0)
{
$params = [
- 'domain' => $domain,
- 'record_id' => $recordId,
- 'sub_domain' => $name,
- 'record_type' => strtoupper($type),
- 'record_line' => '默认',
- 'value' => $value,
- 'ttl' => (int)$ttl,
+ 'Domain' => $domain,
+ 'RecordId' => (int)$recordId,
+ 'SubDomain' => $name,
+ 'RecordType' => strtoupper($type),
+ 'RecordLine' => '默认',
+ 'Value' => $value,
+ 'TTL' => (int)$ttl,
];
- if (strtoupper($type) === 'MX') $params['mx'] = (int)$mx;
+ if (strtoupper($type) === 'MX') $params['MX'] = (int)$mx;
- $res = $this->call('Record.Modify', $params);
- if (empty($res['ok'])) return false;
- $arr = json_decode($res['body'], true);
- if (!is_array($arr) || ($arr['status']['code'] ?? '0') !== '1') {
- $this->lastErr = $arr['status']['message'] ?? 'Record.Modify 失败';
- return false;
- }
- return true;
+ $resp = $this->call('ModifyRecord', $params);
+ return $resp !== false;
}
/**
@@ -124,67 +119,108 @@ class DomainShop_DNSPod
*/
public function deleteRecord($domain, $recordId)
{
- $res = $this->call('Record.Remove', ['domain' => $domain, 'record_id' => $recordId]);
- if (empty($res['ok'])) return false;
- $arr = json_decode($res['body'], true);
- if (!is_array($arr) || ($arr['status']['code'] ?? '0') !== '1') {
- $this->lastErr = $arr['status']['message'] ?? 'Record.Remove 失败';
- return false;
- }
- return true;
+ $resp = $this->call('DeleteRecord', ['Domain' => $domain, 'RecordId' => (int)$recordId]);
+ return $resp !== false;
}
/**
- * 调用 DNSPod API
- * 优先使用 mnbt_http_post(带安全策略),失败回退到 cURL
+ * 调用腾讯云 DNSPod API 3.0
+ * @return array|false 解析后的 Response 数组,失败返回 false(lastErr 记录原因)
*/
private function call($action, array $params)
{
- $url = $this->endpoint . $action;
- $body = http_build_query($params, '', '&');
+ $timestamp = time();
+ $payload = json_encode($params, JSON_UNESCAPED_UNICODE);
+ $authorization = $this->sign($action, $payload, $timestamp);
+
$headers = [
- 'Content-Type: application/x-www-form-urlencoded',
- 'User-Agent: MNBT-DomainShop/1.0',
+ 'Content-Type: application/json; charset=utf-8',
+ 'X-TC-Action: ' . $action,
+ 'X-TC-Timestamp: ' . $timestamp,
+ 'X-TC-Version: ' . $this->version,
+ 'Authorization: ' . $authorization,
];
- // 用插件引擎的 HTTP 出站函数(有内网/协议安全策略)
+ // 用插件引擎的 HTTP 出站函数(有内网/协议安全策略),失败回退到 cURL
if (function_exists('mnbt_http_post')) {
- $res = mnbt_http_post($url, $body, [
+ $res = mnbt_http_post($this->endpoint, $payload, [
'timeout' => 15,
'headers' => $headers,
]);
if (!empty($res['ok'])) {
- return ['ok' => true, 'body' => $res['body'] ?? ''];
+ return $this->parseResponse($res['body'] ?? '');
}
$this->lastErr = $res['error'] ?? 'mnbt_http_post 失败';
- // DNSPod 是公网 API,不会触发内网限制;如果失败回退到 cURL
}
- // 回退:原生 cURL
if (!function_exists('curl_init')) {
$this->lastErr = 'PHP 未启用 cURL 扩展';
- return ['ok' => false, 'error' => $this->lastErr];
+ return false;
}
- $ch = curl_init($url);
+ $ch = curl_init($this->endpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
- CURLOPT_POSTFIELDS => $params,
+ CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
- CURLOPT_HTTPHEADER => ['Expect:'],
+ CURLOPT_HTTPHEADER => $headers,
]);
- // 注意:DNSPod API Token 通过 user token 字段传递
- $params['login_token'] = $this->apiId . ',' . $this->apiSecret;
- $params['format'] = 'json';
- curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params, '', '&'));
$body = curl_exec($ch);
$err = curl_error($ch);
- $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false) {
$this->lastErr = 'cURL 错误:' . $err;
- return ['ok' => false, 'error' => $this->lastErr];
+ return false;
+ }
+ return $this->parseResponse($body);
+ }
+
+ /**
+ * 解析 API 3.0 响应(统一包在 Response 中;存在 Error 视为失败)
+ */
+ private function parseResponse($body)
+ {
+ $arr = json_decode($body, true);
+ if (!is_array($arr) || !isset($arr['Response'])) {
+ $this->lastErr = '响应解析失败:' . mb_substr((string)$body, 0, 200);
+ return false;
+ }
+ $resp = $arr['Response'];
+ if (isset($resp['Error'])) {
+ $this->lastErr = ($resp['Error']['Message'] ?? '') ?: ($resp['Error']['Code'] ?? 'API 错误');
+ return false;
}
- return ['ok' => true, 'body' => $body];
+ return $resp;
+ }
+
+ /**
+ * TC3-HMAC-SHA256 签名(腾讯云 API 3.0 签名方法 v3)
+ */
+ private function sign($action, $payload, $timestamp)
+ {
+ $algorithm = 'TC3-HMAC-SHA256';
+ $host = 'dnspod.tencentcloudapi.com';
+ $date = gmdate('Y-m-d', $timestamp);
+
+ // 1. 拼接规范请求串
+ $contentType = 'application/json; charset=utf-8';
+ $canonicalHeaders = "content-type:$contentType\nhost:$host\nx-tc-action:" . strtolower($action) . "\n";
+ $signedHeaders = 'content-type;host;x-tc-action';
+ $hashedRequestPayload = hash('sha256', $payload);
+ $canonicalRequest = "POST\n/\n\n$canonicalHeaders\n$signedHeaders\n$hashedRequestPayload";
+
+ // 2. 拼接待签名字符串
+ $credentialScope = "$date/{$this->service}/tc3_request";
+ $hashedCanonicalRequest = hash('sha256', $canonicalRequest);
+ $stringToSign = "$algorithm\n$timestamp\n$credentialScope\n$hashedCanonicalRequest";
+
+ // 3. 计算派生签名密钥与签名
+ $secretDate = hash_hmac('sha256', $date, 'TC3' . $this->apiSecret, true);
+ $secretService = hash_hmac('sha256', $this->service, $secretDate, true);
+ $secretSigning = hash_hmac('sha256', 'tc3_request', $secretService, true);
+ $signature = hash_hmac('sha256', $stringToSign, $secretSigning);
+
+ // 4. 拼接 Authorization
+ return "$algorithm Credential={$this->apiId}/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature";
}
}