feat: initial commit

This commit is contained in:
root
2026-06-14 14:00:24 +08:00
commit 72d388f642
118 changed files with 39015 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
<?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);
}
}