171 lines
6.3 KiB
PHP
Executable File
171 lines
6.3 KiB
PHP
Executable File
<?php
|
||
require_once __DIR__ . '/CloudflareConfig.php';
|
||
|
||
/**
|
||
* Cloudflare API v4 客户端(Zone / DNS / SSL)
|
||
*/
|
||
class CloudflareClient
|
||
{
|
||
private $token;
|
||
private $accountId;
|
||
|
||
public function __construct($token = null, $accountId = null)
|
||
{
|
||
$this->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);
|
||
}
|
||
}
|