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)), '.'); } }