域名管理

This commit is contained in:
root
2026-06-02 00:32:05 +08:00
parent f2ec634888
commit 4aec0b4fce
20 changed files with 1879 additions and 7 deletions
+331
View File
@@ -0,0 +1,331 @@
<?php
declare(strict_types=1);
namespace app\common\service;
use fast\Http;
use think\Exception;
/**
* Cloudflare API 服务层
*/
class CloudflareService
{
private const API_BASE = 'https://api.cloudflare.com/client/v4';
private string $apiToken;
private string $accountId;
private string $serverIp;
public function __construct()
{
$config = config('cloudflare');
$this->apiToken = (string)($config['api_token'] ?? '');
$this->accountId = (string)($config['account_id'] ?? '');
$this->serverIp = trim((string)($config['server_ip'] ?? ''));
if ($this->apiToken === '' || $this->accountId === '') {
throw new Exception('Cloudflare API 凭证未配置,请在 .env 中设置 cloudflare.api_token 与 cloudflare.account_id');
}
}
/**
* 创建 Cloudflare Zone
*
* @param string $domain 根域名
* @return array{zone_id: string, name_servers: array, status: string}
* @throws Exception
*/
public function createZone(string $domain): array
{
$body = [
'name' => $domain,
'account' => ['id' => $this->accountId],
'type' => 'full',
];
$response = $this->request('POST', '/zones', $body);
$result = $response['result'] ?? [];
return [
'zone_id' => (string)($result['id'] ?? ''),
'name_servers' => (array)($result['name_servers'] ?? []),
'status' => (string)($result['status'] ?? 'pending'),
];
}
/**
* @throws Exception
*/
public function getZone(string $zoneId): array
{
$response = $this->request('GET', '/zones/' . $zoneId);
return (array)($response['result'] ?? []);
}
/**
* @throws Exception
*/
public function deleteZone(string $zoneId): bool
{
$this->request('DELETE', '/zones/' . $zoneId);
return true;
}
/**
* @throws Exception
*/
public function listDnsRecords(string $zoneId): array
{
$response = $this->request('GET', '/zones/' . $zoneId . '/dns_records', [], ['per_page' => 100]);
return (array)($response['result'] ?? []);
}
/**
* 聚合检测三项状态
* Zone/NS 检测通过后,自动将根域名 A 记录指向 server_ip 并校验
*
* @param array $row 域名记录
* @return array{zone_status: string, ns_status: string, dns_status: string, check_result: string}
*/
public function detectDomain(array $row): array
{
$domain = strtolower(trim((string)($row['domain'] ?? '')));
$zoneId = (string)($row['zone_id'] ?? '');
$expectedNs = $this->decodeNameservers($row['nameservers'] ?? '');
$zoneStatus = 'failed';
$nsStatus = 'pending';
$dnsStatus = 'pending';
$messages = [];
try {
$zone = $this->getZone($zoneId);
$cfStatus = (string)($zone['status'] ?? '');
if ($cfStatus === 'active') {
$zoneStatus = 'active';
$messages[] = 'Zone:已激活';
} elseif (in_array($cfStatus, ['pending', 'initializing'], true)) {
$zoneStatus = 'pending';
$messages[] = 'Zone:待激活';
} else {
$zoneStatus = 'failed';
$messages[] = 'Zone:异常(' . $cfStatus . ')';
}
} catch (\Throwable $e) {
$zoneStatus = 'failed';
$messages[] = 'Zone:检测失败(' . $e->getMessage() . ')';
}
$currentNs = $this->getDomainNameservers($domain);
if ($currentNs === []) {
$nsStatus = 'pending';
$messages[] = 'NS:待验证(未查询到NS记录)';
} elseif ($this->compareNameservers($currentNs, $expectedNs)) {
$nsStatus = 'verified';
$messages[] = 'NS:已验证';
} else {
$nsStatus = 'pending';
$messages[] = 'NS:待验证(当前NS与Cloudflare不一致)';
}
if ($zoneStatus !== 'active') {
$dnsStatus = 'pending';
$messages[] = 'DNS:待创建(Zone未激活)';
} elseif ($nsStatus !== 'verified') {
$dnsStatus = 'pending';
$messages[] = 'DNS:待创建(NS未验证)';
} elseif ($this->serverIp === '') {
$dnsStatus = 'failed';
$messages[] = 'DNS:未配置server_ip';
} elseif (!filter_var($this->serverIp, FILTER_VALIDATE_IP)) {
$dnsStatus = 'failed';
$messages[] = 'DNS:server_ip格式无效';
} else {
try {
if ($this->hasRootCnameConflict($zoneId, $domain)) {
$dnsStatus = 'failed';
$messages[] = 'DNS:根域名存在CNAME记录,无法创建A记录';
} else {
$this->upsertRootARecord($zoneId, $domain, $this->serverIp);
if ($this->verifyRootARecord($zoneId, $domain, $this->serverIp)) {
$dnsStatus = 'created';
$messages[] = 'DNS:已创建(A=' . $this->serverIp . ')';
} else {
$dnsStatus = 'pending';
$messages[] = 'DNS:待创建(A记录未指向' . $this->serverIp . ')';
}
}
} catch (\Throwable $e) {
$dnsStatus = 'failed';
$messages[] = 'DNS:操作失败(' . $e->getMessage() . ')';
}
}
return [
'zone_status' => $zoneStatus,
'ns_status' => $nsStatus,
'dns_status' => $dnsStatus,
'check_result' => implode('; ', $messages),
];
}
/**
* @throws Exception
*/
private function listRootARecords(string $zoneId, string $domain): array
{
$response = $this->request('GET', '/zones/' . $zoneId . '/dns_records', [], [
'type' => 'A',
'name' => $domain,
]);
return (array)($response['result'] ?? []);
}
/**
* @throws Exception
*/
private function hasRootCnameConflict(string $zoneId, string $domain): bool
{
$response = $this->request('GET', '/zones/' . $zoneId . '/dns_records', [], [
'type' => 'CNAME',
'name' => $domain,
]);
return count((array)($response['result'] ?? [])) > 0;
}
/**
* @throws Exception
*/
private function upsertRootARecord(string $zoneId, string $domain, string $ip): void
{
$records = $this->listRootARecords($zoneId, $domain);
if ($records === []) {
$this->request('POST', '/zones/' . $zoneId . '/dns_records', [
'type' => 'A',
'name' => $domain,
'content' => $ip,
'ttl' => 1,
'proxied' => false,
]);
return;
}
foreach ($records as $record) {
$recordId = (string)($record['id'] ?? '');
$content = (string)($record['content'] ?? '');
if ($recordId === '' || $content === $ip) {
continue;
}
$this->request('PATCH', '/zones/' . $zoneId . '/dns_records/' . $recordId, [
'content' => $ip,
'ttl' => 1,
'proxied' => false,
]);
}
}
/**
* @throws Exception
*/
private function verifyRootARecord(string $zoneId, string $domain, string $ip): bool
{
$records = $this->listRootARecords($zoneId, $domain);
foreach ($records as $record) {
if ((string)($record['content'] ?? '') === $ip) {
return true;
}
}
return false;
}
/**
* @throws Exception
*/
private function request(string $method, string $path, array $body = [], array $query = []): array
{
$url = self::API_BASE . $path;
if ($query !== []) {
$url .= '?' . http_build_query($query);
}
$options = [
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->apiToken,
'Content-Type: application/json',
],
];
$payload = $body !== [] ? json_encode($body, JSON_UNESCAPED_UNICODE) : '';
if (strtoupper($method) === 'GET') {
$result = Http::sendRequest($url, [], 'GET', $options);
} elseif (strtoupper($method) === 'DELETE') {
$result = Http::sendRequest($url, $payload, 'DELETE', $options);
} else {
$result = Http::sendRequest($url, $payload, strtoupper($method), $options);
}
if (!$result['ret']) {
throw new Exception('Cloudflare API 请求失败: ' . ($result['msg'] ?? '未知错误'));
}
$decoded = json_decode((string)$result['msg'], true);
if (!is_array($decoded)) {
throw new Exception('Cloudflare API 响应解析失败');
}
if (empty($decoded['success'])) {
$errors = $decoded['errors'] ?? [];
$message = 'Cloudflare API 错误';
if (is_array($errors) && isset($errors[0]['message'])) {
$message = (string)$errors[0]['message'];
}
throw new Exception($message);
}
return $decoded;
}
private function decodeNameservers($nameservers): array
{
if (is_array($nameservers)) {
return $nameservers;
}
if (is_string($nameservers) && $nameservers !== '') {
$decoded = json_decode($nameservers, true);
return is_array($decoded) ? $decoded : [];
}
return [];
}
private function getDomainNameservers(string $domain): array
{
$records = @dns_get_record($domain, DNS_NS);
if (!is_array($records)) {
return [];
}
$list = [];
foreach ($records as $record) {
if (!empty($record['target'])) {
$list[] = $this->normalizeNs((string)$record['target']);
}
}
return array_values(array_unique($list));
}
private function compareNameservers(array $current, array $expected): bool
{
if ($expected === []) {
return false;
}
$currentNorm = array_map([$this, 'normalizeNs'], $current);
$expectedNorm = array_map([$this, 'normalizeNs'], $expected);
sort($currentNorm);
sort($expectedNorm);
return $currentNorm === $expectedNorm;
}
private function normalizeNs(string $ns): string
{
return rtrim(strtolower(trim($ns)), '.');
}
}