token = $token ?? CloudflareConfig::apiToken(); $this->accountId = $accountId ?? CloudflareConfig::accountId(); } /** * @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int} */ public function createZone($hostname) { $res = $this->request('POST', '/zones', [ 'name' => $hostname, 'account' => ['id' => $this->accountId], 'type' => 'full', ]); if (!$res['ok'] && (int) ($res['code'] ?? 0) === 1061) { return $this->findZoneByName($hostname); } return $res; } /** * @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int} */ public function findZoneByName($hostname) { $path = '/zones?name=' . rawurlencode($hostname) . '&account.id=' . rawurlencode($this->accountId); $res = $this->request('GET', $path); if (!$res['ok']) { return $res; } $zones = $res['result'] ?? []; if (!is_array($zones) || empty($zones[0])) { return ['ok' => false, 'error' => 'Zone 不存在']; } return ['ok' => true, 'result' => $zones[0]]; } /** * @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int} */ public function getZone($zoneId) { return $this->request('GET', '/zones/' . rawurlencode($zoneId)); } /** * @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int} */ public function deleteZone($zoneId) { return $this->request('DELETE', '/zones/' . rawurlencode($zoneId)); } /** * @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int} */ public function upsertARecord($zoneId, $fqdn, $ip, $proxied = true) { $path = '/zones/' . rawurlencode($zoneId) . '/dns_records?type=A&name=' . rawurlencode($fqdn); $list = $this->request('GET', $path); if (!$list['ok']) { return $list; } $records = is_array($list['result'] ?? null) ? $list['result'] : []; $existing = $records[0] ?? null; $payload = [ 'type' => 'A', 'name' => $fqdn, 'content' => $ip, 'proxied' => (bool) $proxied, 'ttl' => 1, ]; if (is_array($existing) && !empty($existing['id'])) { return $this->request('PATCH', '/zones/' . rawurlencode($zoneId) . '/dns_records/' . rawurlencode($existing['id']), $payload); } return $this->request('POST', '/zones/' . rawurlencode($zoneId) . '/dns_records', $payload); } /** * @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int} */ public function setSslFlexible($zoneId) { return $this->request('PATCH', '/zones/' . rawurlencode($zoneId) . '/settings/ssl', ['value' => 'flexible']); } /** * @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int} */ public function setAlwaysHttps($zoneId) { return $this->request('PATCH', '/zones/' . rawurlencode($zoneId) . '/settings/always_use_https', ['value' => 'on']); } /** * @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int} */ private function request($method, $path, $body = null) { $url = 'https://api.cloudflare.com/client/v4' . $path; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); $headers = [ 'Authorization: Bearer ' . $this->token, 'Content-Type: application/json', ]; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE)); } if (function_exists('cloak_curl_apply_upstream_defaults')) { cloak_curl_apply_upstream_defaults($ch, 5, 20); } else { curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); curl_setopt($ch, CURLOPT_TIMEOUT, 20); } $raw = curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($raw === false) { $err = curl_error($ch); $errno = curl_errno($ch); curl_close($ch); self::logError($method, $path, 'curl', $errno . ':' . $err, $httpCode); return ['ok' => false, 'error' => 'API请求失败:' . $err . '(errno:' . $errno . ')', 'http_code' => $httpCode]; } curl_close($ch); if ($raw === '') { self::logError($method, $path, 'empty_body', '', $httpCode); return ['ok' => false, 'error' => 'API响应为空(HTTP ' . $httpCode . ')', 'http_code' => $httpCode]; } $decoded = json_decode($raw, true); if (!is_array($decoded)) { self::logError($method, $path, 'non_json', substr($raw, 0, 200), $httpCode); return ['ok' => false, 'error' => 'API响应非JSON(HTTP ' . $httpCode . ')', 'http_code' => $httpCode]; } if (empty($decoded['success'])) { $msg = $decoded['errors'][0]['message'] ?? '未知错误'; $code = (int) ($decoded['errors'][0]['code'] ?? 0); self::logError($method, $path, 'api_error', $msg, $httpCode); return ['ok' => false, 'error' => $msg, 'code' => $code, 'http_code' => $httpCode]; } return ['ok' => true, 'result' => $decoded['result'] ?? null, 'http_code' => $httpCode]; } private static function logError($method, $path, $kind, $detail, $httpCode) { $line = date('Y-m-d H:i:s') . ' | Cloudflare ' . $method . ' ' . $path . ' | ' . $kind . ' | HTTP ' . $httpCode . ' | ' . $detail . "\n"; @file_put_contents(dirname(__DIR__, 2) . '/err.txt', $line, FILE_APPEND); } }