AliYun.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. <?php
  2. namespace App\Utils\DDNS;
  3. use App\Utils\Library\Templates\DNS;
  4. use Arr;
  5. use Cache;
  6. use Http;
  7. use Log;
  8. use RuntimeException;
  9. class AliYun implements DNS
  10. {
  11. // 开发依据: https://api.aliyun.com/document/Alidns/2015-01-09/overview
  12. private const API_ENDPOINT = 'https://alidns.aliyuncs.com/';
  13. public const KEY = 'aliyun';
  14. public const LABEL = 'Alibaba Cloud | 阿里云';
  15. private string $accessKeyID;
  16. private string $accessKeySecret;
  17. public function __construct(private readonly string $subdomain)
  18. {
  19. $this->accessKeyID = sysConfig('ddns_key');
  20. $this->accessKeySecret = sysConfig('ddns_secret');
  21. }
  22. public function store(string $ip, string $type): bool
  23. {
  24. $domainInfo = $this->parseDomainInfo();
  25. if (! $domainInfo) {
  26. return false;
  27. }
  28. return (bool) $this->sendRequest('AddDomainRecord', [
  29. 'DomainName' => $domainInfo['domain'],
  30. 'RR' => $domainInfo['sub'],
  31. 'Type' => $type,
  32. 'Value' => $ip,
  33. ]);
  34. }
  35. private function parseDomainInfo(): array
  36. {
  37. $domains = Cache::remember('ddns_get_domains', now()->addHour(), function () {
  38. return array_column($this->sendRequest('DescribeDomains')['Domains']['Domain'] ?? [], 'DomainName');
  39. });
  40. if ($domains) {
  41. $matched = Arr::first($domains, fn ($domain) => str_contains($this->subdomain, $domain));
  42. }
  43. if (empty($matched)) {
  44. throw new RuntimeException('['.self::LABEL." — DescribeDomains] The subdomain $this->subdomain does not match any domain in your account.");
  45. }
  46. return [
  47. 'sub' => rtrim(substr($this->subdomain, 0, -strlen($matched)), '.'),
  48. 'domain' => $matched,
  49. ];
  50. }
  51. private function sendRequest(string $action, array $parameters = []): array
  52. {
  53. $parameters += [
  54. 'Action' => $action,
  55. 'Format' => 'JSON',
  56. 'Version' => '2015-01-09',
  57. 'AccessKeyId' => $this->accessKeyID,
  58. 'SignatureMethod' => 'HMAC-SHA1',
  59. 'Timestamp' => gmdate("Y-m-d\TH:i:s\Z"), // 公共参数Timestamp GMT时间
  60. 'SignatureVersion' => '1.0',
  61. 'SignatureNonce' => str_replace('.', '', microtime(true)), // 唯一数,用于防止网络重放攻击
  62. ];
  63. $parameters['Signature'] = $this->generateSignature($parameters);
  64. $response = Http::asForm()->timeout(15)->post(self::API_ENDPOINT, $parameters);
  65. $data = $response->json();
  66. if ($data) {
  67. if ($response->successful()) {
  68. return $data;
  69. }
  70. Log::error('['.self::LABEL." — $action] 返回错误信息: ".$data['Message'] ?? 'Unknown error');
  71. } else {
  72. Log::error('['.self::LABEL." — $action] 请求失败");
  73. }
  74. exit(400);
  75. }
  76. private function generateSignature(array $parameters): string
  77. { // 签名
  78. ksort($parameters, SORT_STRING);
  79. $stringToBeSigned = 'POST&%2F&'.urlencode(http_build_query($parameters));
  80. return base64_encode(hash_hmac('sha1', $stringToBeSigned, $this->accessKeySecret.'&', true));
  81. }
  82. public function update(string $latest_ip, string $original_ip, string $type): bool
  83. {
  84. $recordIds = $this->getRecordIds($type, $original_ip);
  85. $domainInfo = $this->parseDomainInfo();
  86. if ($recordIds && $domainInfo) {
  87. $ret = $this->sendRequest('UpdateDomainRecord', ['RR' => $domainInfo['sub'], 'RecordId' => Arr::first($recordIds), 'Type' => $type, 'Value' => $latest_ip]);
  88. }
  89. return (bool) ($ret ?? false);
  90. }
  91. private function getRecordIds(string $type, string $ip): array
  92. { // 域名信息
  93. $parameters = ['SubDomain' => $this->subdomain];
  94. if ($type) {
  95. $parameters['Type'] = $type;
  96. }
  97. $records = $this->sendRequest('DescribeSubDomainRecords', $parameters)['DomainRecords']['Record'] ?? [];
  98. if ($ip) {
  99. $records = array_filter($records, static function ($record) use ($ip) {
  100. return $record['Value'] === $ip;
  101. });
  102. }
  103. return array_column($records, 'RecordId');
  104. }
  105. public function destroy(string $type, string $ip): int
  106. {
  107. $recordIds = $this->getRecordIds($type, $ip);
  108. $deletedCount = 0;
  109. foreach ($recordIds as $recordId) {
  110. if ($this->sendRequest('DeleteDomainRecord', ['RecordId' => $recordId])) {
  111. $deletedCount++;
  112. }
  113. }
  114. return $deletedCount;
  115. }
  116. }