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/DNS 状态;NS 已验证且 Zone 已激活时,静默校验并修正 A 记录、Proxied、SSL、HTTPS * * @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 { if ($this->hasRootCnameConflict($zoneId, $domain)) { $dnsStatus = 'failed'; $messages[] = 'DNS:根域名存在CNAME记录,无法创建A记录'; } else { try { $this->reconcileCloudflareConfig($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; } /** * NS 已验证且 Zone 已激活时:读取 Proxied / SSL / HTTPS / A 记录,与期望值不一致则修正 * * @throws Exception */ private function reconcileCloudflareConfig(string $zoneId, string $domain, string $ip): void { $this->ensureZoneEdgeSettings($zoneId); $this->upsertRootARecord($zoneId, $domain, $ip); } /** * 读取 Zone 单项设置值 * * @throws Exception */ private function getZoneSettingValue(string $zoneId, string $settingId): string { $response = $this->request('GET', '/zones/' . $zoneId . '/settings/' . $settingId); return strtolower(trim((string)($response['result']['value'] ?? ''))); } /** * SSL/TLS=Flexible、Always Use HTTPS=开启;已与期望一致则跳过 PATCH * * @throws Exception */ private function ensureZoneEdgeSettings(string $zoneId): void { if ($this->getZoneSettingValue($zoneId, 'ssl') !== 'flexible') { $this->request('PATCH', '/zones/' . $zoneId . '/settings/ssl', [ 'value' => 'flexible', ]); } if ($this->getZoneSettingValue($zoneId, 'always_use_https') !== 'on') { $this->request('PATCH', '/zones/' . $zoneId . '/settings/always_use_https', [ 'value' => 'on', ]); } } /** * 创建或更新根域名 A 记录(IP 与 Proxied 与期望不一致时修正) * * @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' => true, ]); return; } foreach ($records as $record) { $recordId = (string)($record['id'] ?? ''); $content = (string)($record['content'] ?? ''); $proxied = (bool)($record['proxied'] ?? false); if ($recordId === '') { continue; } if ($content === $ip && $proxied) { continue; } $this->request('PATCH', '/zones/' . $zoneId . '/dns_records/' . $recordId, [ 'content' => $ip, 'ttl' => 1, 'proxied' => true, ]); } } /** * 校验根域名 A 记录:content 指向 server_ip 且已开启 Proxy * * @throws Exception */ private function verifyRootARecord(string $zoneId, string $domain, string $ip): bool { $records = $this->listRootARecords($zoneId, $domain); foreach ($records as $record) { $content = (string)($record['content'] ?? ''); $proxied = (bool)($record['proxied'] ?? false); if ($content === $ip && $proxied) { 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)), '.'); } }