Namecheap.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. <?php
  2. namespace App\Utils\DDNS;
  3. use App\Utils\IP;
  4. use App\Utils\Library\Templates\DNS;
  5. use Arr;
  6. use Cache;
  7. use Http;
  8. use Log;
  9. use RuntimeException;
  10. class Namecheap implements DNS
  11. {
  12. // 开发依据: https://www.namecheap.com/support/api/methods/
  13. private const API_ENDPOINT = 'https://api.namecheap.com/xml.response';
  14. public const KEY = 'namecheap';
  15. public const LABEL = 'Namecheap';
  16. private string $username;
  17. private string $apiKey;
  18. private array $domainInfo;
  19. private array $domainRecords;
  20. public function __construct(private readonly string $subdomain)
  21. {
  22. $this->username = sysConfig('ddns_key');
  23. $this->apiKey = sysConfig('ddns_secret');
  24. $this->domainInfo = $this->parseDomainInfo();
  25. $this->domainRecords = $this->fetchDomainRecords();
  26. }
  27. private function parseDomainInfo(): array
  28. {
  29. $domains = Cache::remember('ddns_get_domains', now()->addHour(), function () {
  30. return array_map(static function ($domain) {
  31. return $domain['@attributes']['Name'];
  32. }, $this->sendRequest('namecheap.domains.getList')['DomainGetListResult']['Domain']);
  33. });
  34. if ($domains) {
  35. $matched = Arr::first($domains, fn ($domain) => str_contains($this->subdomain, $domain));
  36. }
  37. if (empty($matched)) {
  38. throw new RuntimeException('['.self::LABEL." — domains.getList] The subdomain $this->subdomain does not match any domain in your account.");
  39. }
  40. $domainParts = explode('.', $matched);
  41. return [
  42. 'sub' => rtrim(substr($this->subdomain, 0, -strlen($matched)), '.'),
  43. 'domain' => $domainParts[0],
  44. 'tld' => end($domainParts),
  45. ];
  46. }
  47. private function sendRequest(string $action, array $parameters = []): array
  48. {
  49. $response = Http::timeout(15)->retry(3, 1000)->get(self::API_ENDPOINT, ['ApiUser' => $this->username, 'ApiKey' => $this->apiKey, 'UserName' => $this->username, 'ClientIp' => IP::getClientIP(), 'Command' => $action, ...$parameters]);
  50. $data = $response->body();
  51. if ($data) {
  52. $data = json_decode(json_encode(simplexml_load_string($data)), true);
  53. if ($response->successful() && $data['@attributes']['Status'] === 'OK') {
  54. return $data['CommandResponse'];
  55. }
  56. Log::error('['.self::LABEL." — $action] 返回错误信息: ".$data['Errors']['Error'] ?? 'Unknown error');
  57. } else {
  58. Log::error('['.self::LABEL." — $action] 请求失败");
  59. }
  60. exit(400);
  61. }
  62. private function fetchDomainRecords(): array
  63. {
  64. $records = $this->sendRequest('namecheap.domains.dns.getHosts', ['SLD' => $this->domainInfo['domain'], 'TLD' => $this->domainInfo['tld']]);
  65. if (isset($records['DomainDNSGetHostsResult']['host'])) {
  66. $hosts = $records['DomainDNSGetHostsResult']['host'];
  67. if (isset($hosts[0])) {
  68. foreach ($hosts as $record) {
  69. $ret[] = $this->parseRecordData($record);
  70. }
  71. } else {
  72. $ret[] = $this->parseRecordData($hosts);
  73. }
  74. }
  75. return $ret ?? [];
  76. }
  77. private function parseRecordData(array $record): array
  78. {
  79. return [
  80. 'name' => $record['@attributes']['Name'],
  81. 'type' => $record['@attributes']['Type'],
  82. 'address' => $record['@attributes']['Address'],
  83. 'ttl' => $record['@attributes']['TTL'],
  84. ];
  85. }
  86. public function store(string $ip, string $type): bool
  87. {
  88. $this->domainRecords[] = [
  89. 'name' => $this->domainInfo['sub'],
  90. 'type' => $type,
  91. 'address' => $ip,
  92. 'ttl' => '60',
  93. ];
  94. return $this->updateDomainRecords();
  95. }
  96. private function updateDomainRecords(): bool
  97. {
  98. $para = ['SLD' => $this->domainInfo['domain'], 'TLD' => $this->domainInfo['tld']];
  99. foreach ($this->domainRecords as $index => $record) {
  100. $para['HostName'.($index + 1)] = $record['name'];
  101. $para['RecordType'.($index + 1)] = $record['type'];
  102. $para['Address'.($index + 1)] = $record['address'];
  103. $para['TTL'.($index + 1)] = $record['ttl'];
  104. }
  105. return $this->sendRequest('namecheap.domains.dns.setHosts', $para)['DomainDNSSetHostsResult']['@attributes']['IsSuccess'] === 'true';
  106. }
  107. public function update(string $latest_ip, string $original_ip, string $type): bool
  108. {
  109. foreach ($this->domainRecords as &$record) {
  110. if ($record['address'] === $original_ip && $record['name'] === $this->domainInfo['sub'] && $record['type'] === $type) {
  111. $record['address'] = $latest_ip;
  112. return $this->updateDomainRecords();
  113. }
  114. }
  115. return false;
  116. }
  117. public function destroy(string $type, string $ip): int|bool
  118. {
  119. if ($ip) {
  120. $this->domainRecords = array_filter($this->domainRecords, function ($record) use ($ip) {
  121. return $record['address'] !== $ip || $record['name'] !== $this->domainInfo['sub'];
  122. });
  123. } elseif ($type) {
  124. $this->domainRecords = array_filter($this->domainRecords, function ($record) use ($type) {
  125. return $record['type'] !== $type || $record['name'] !== $this->domainInfo['sub'];
  126. });
  127. } else {
  128. $this->domainRecords = array_filter($this->domainRecords, function ($record) {
  129. return $record['name'] !== $this->domainInfo['sub'];
  130. });
  131. }
  132. return $this->updateDomainRecords();
  133. }
  134. }