Files
CLOAK/lib/Cloak/DomainProvisioningService.php
T

310 lines
11 KiB
PHP
Raw Normal View History

2026-06-14 14:00:24 +08:00
<?php
require_once __DIR__ . '/DomainRepository.php';
require_once __DIR__ . '/CloudflareConfig.php';
require_once __DIR__ . '/CloudflareClient.php';
/**
* 域名添加 / NS 检测 / DNS+SSL 配置 / 删除编排
*/
class DomainProvisioningService
{
/** @var CloudflareClient|null CLI 回归注入 Mock 客户端 */
private static $clientOverride = null;
public static function setClientOverride($client)
{
self::$clientOverride = $client;
}
public static function clearClientOverride()
{
self::$clientOverride = null;
}
private static function newClient()
{
return self::$clientOverride !== null ? self::$clientOverride : new CloudflareClient();
}
/**
* 添加域名:本地入库 + 可选创建 Cloudflare Zone
*
2026-07-06 10:34:34 +08:00
* @param string $dnsMode auto|manual — manual 时不调用 Cloudflare
* @return array{ok:bool,error?:string,hostname?:string,id?:int,nameservers?:array,cf_status?:string,dns_mode?:string}
2026-06-14 14:00:24 +08:00
*/
2026-07-06 10:34:34 +08:00
public static function provisionOnAdd(PDO $pdo, $hostname, $dnsMode = 'auto')
2026-06-14 14:00:24 +08:00
{
$add = DomainRepository::add($pdo, $hostname);
if (!$add['ok']) {
return $add;
}
2026-07-06 10:34:34 +08:00
$id = (int) $add['id'];
$host = $add['hostname'];
$dnsMode = strtolower(trim((string) $dnsMode)) === 'manual' ? 'manual' : 'auto';
2026-06-14 14:00:24 +08:00
2026-07-06 10:34:34 +08:00
if ($dnsMode === 'manual' || !CloudflareConfig::isEnabled()) {
$status = $dnsMode === 'manual' ? 'manual' : 'local';
2026-06-14 14:00:24 +08:00
DomainRepository::updateCloudflareMeta($pdo, $id, [
2026-07-06 10:34:34 +08:00
'cf_status' => $status,
'cf_error' => '',
2026-06-14 14:00:24 +08:00
]);
2026-07-06 10:34:34 +08:00
2026-06-14 14:00:24 +08:00
return [
2026-07-06 10:34:34 +08:00
'ok' => true,
'id' => $id,
'hostname' => $host,
'cf_status' => $status,
'dns_mode' => $dnsMode,
2026-06-14 14:00:24 +08:00
'nameservers' => [],
];
}
$client = self::newClient();
$zone = $client->createZone($host);
if (!$zone['ok']) {
DomainRepository::updateCloudflareMeta($pdo, $id, [
'cf_status' => 'error',
'cf_error' => $zone['error'] ?? '创建 Zone 失败',
]);
return [
'ok' => false,
'error' => $zone['error'] ?? 'Cloudflare 创建 Zone 失败',
'id' => $id,
'hostname' => $host,
];
}
$result = $zone['result'];
$ns = is_array($result['name_servers'] ?? null) ? $result['name_servers'] : [];
DomainRepository::updateCloudflareMeta($pdo, $id, [
'cf_zone_id' => (string) ($result['id'] ?? ''),
'cf_nameservers' => json_encode($ns, JSON_UNESCAPED_UNICODE),
'cf_status' => 'pending_ns',
'cf_error' => '',
'cf_checked_at' => date('Y-m-d H:i:s'),
]);
return [
'ok' => true,
'id' => $id,
'hostname' => $host,
'cf_status' => 'pending_ns',
2026-07-06 10:34:34 +08:00
'dns_mode' => 'auto',
2026-06-14 14:00:24 +08:00
'nameservers' => $ns,
];
}
2026-07-06 10:34:34 +08:00
/**
* 批量添加域名
*
* @return array{ok:bool,error?:string,added?:array,failed?:array,dns_mode?:string}
*/
public static function provisionBatch(PDO $pdo, $hostnameText, $dnsMode = 'auto')
{
$hosts = DomainRepository::parseHostnameBatch($hostnameText);
if ($hosts === []) {
return ['ok' => false, 'error' => '请至少输入一个有效域名(每行一个)'];
}
$dnsMode = strtolower(trim((string) $dnsMode)) === 'manual' ? 'manual' : 'auto';
$added = [];
$failed = [];
foreach ($hosts as $host) {
$res = self::provisionOnAdd($pdo, $host, $dnsMode);
if (!empty($res['ok'])) {
$added[] = $res;
} else {
$failed[] = [
'hostname' => $host,
'error' => $res['error'] ?? '添加失败',
];
}
}
if ($added === []) {
$firstErr = $failed[0]['error'] ?? '添加失败';
return ['ok' => false, 'error' => $firstErr, 'added' => [], 'failed' => $failed, 'dns_mode' => $dnsMode];
}
return [
'ok' => true,
'added' => $added,
'failed' => $failed,
'dns_mode' => $dnsMode,
];
}
2026-06-14 14:00:24 +08:00
/**
* 手动检测 NS 是否生效,生效后配置 A 记录与 SSL
*
* @return array{ok:bool,status?:string,message?:string,nameservers?:array}
*/
public static function checkAndProvision(PDO $pdo, $domainId)
{
$row = DomainRepository::findById($pdo, $domainId);
if (!$row) {
return ['ok' => false, 'message' => '域名不存在'];
}
2026-07-06 10:34:34 +08:00
if (($row['cf_status'] ?? '') === 'manual') {
return ['ok' => false, 'message' => '该域名为手动解析模式,无需 Cloudflare 检测'];
}
2026-06-14 14:00:24 +08:00
if (!CloudflareConfig::isEnabled()) {
return ['ok' => false, 'message' => '未配置 Cloudflare API,无法检测'];
}
$client = self::newClient();
$zoneId = trim((string) ($row['cf_zone_id'] ?? ''));
if ($zoneId === '') {
$zone = $client->createZone($row['hostname']);
if (!$zone['ok']) {
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
'cf_status' => 'error',
'cf_error' => $zone['error'] ?? '补创建 Zone 失败',
'cf_checked_at' => date('Y-m-d H:i:s'),
]);
return ['ok' => false, 'message' => $zone['error'] ?? '补创建 Zone 失败'];
}
$result = $zone['result'];
$zoneId = (string) ($result['id'] ?? '');
$ns = is_array($result['name_servers'] ?? null) ? $result['name_servers'] : [];
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
'cf_zone_id' => $zoneId,
'cf_nameservers' => json_encode($ns, JSON_UNESCAPED_UNICODE),
'cf_status' => 'pending_ns',
'cf_error' => '',
'cf_checked_at' => date('Y-m-d H:i:s'),
]);
$row = DomainRepository::findById($pdo, $domainId);
}
$zoneRes = $client->getZone($zoneId);
if (!$zoneRes['ok']) {
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
'cf_status' => 'error',
'cf_error' => $zoneRes['error'] ?? '读取 Zone 失败',
'cf_checked_at' => date('Y-m-d H:i:s'),
]);
return ['ok' => false, 'message' => $zoneRes['error'] ?? '读取 Zone 失败'];
}
$zoneData = $zoneRes['result'];
$status = strtolower((string) ($zoneData['status'] ?? 'pending'));
$ns = CloudflareConfig::parseNameservers($row['cf_nameservers'] ?? '');
if (empty($ns) && is_array($zoneData['name_servers'] ?? null)) {
$ns = $zoneData['name_servers'];
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
'cf_nameservers' => json_encode($ns, JSON_UNESCAPED_UNICODE),
]);
}
if ($status !== 'active') {
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
'cf_status' => 'pending_ns',
'cf_checked_at' => date('Y-m-d H:i:s'),
'cf_error' => '',
]);
return [
'ok' => true,
'status' => 'pending_ns',
'message' => 'Nameserver 尚未生效,请到注册商修改为:' . implode('、', $ns),
'nameservers' => $ns,
];
}
$serverIp = CloudflareConfig::serverIp();
if ($serverIp === '') {
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
'cf_status' => 'error',
'cf_error' => 'SERVER_IP 未配置,无法添加 DNS 记录',
'cf_checked_at' => date('Y-m-d H:i:s'),
]);
return ['ok' => false, 'message' => 'SERVER_IP 未配置,请在 cong.php 中填写服务器公网 IP'];
}
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
'cf_status' => 'provisioning',
'cf_error' => '',
]);
$host = $row['hostname'];
foreach ([$host, 'www.' . $host] as $fqdn) {
$dns = $client->upsertARecord($zoneId, $fqdn, $serverIp, true);
if (!$dns['ok']) {
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
'cf_status' => 'error',
'cf_error' => 'DNS 记录失败(' . $fqdn . '): ' . ($dns['error'] ?? ''),
'cf_checked_at' => date('Y-m-d H:i:s'),
]);
return ['ok' => false, 'message' => $dns['error'] ?? 'DNS 配置失败'];
}
}
$ssl = $client->setSslFlexible($zoneId);
if (!$ssl['ok']) {
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
'cf_status' => 'error',
'cf_error' => 'SSL Flexible 失败: ' . ($ssl['error'] ?? ''),
'cf_checked_at' => date('Y-m-d H:i:s'),
]);
return ['ok' => false, 'message' => $ssl['error'] ?? 'SSL 配置失败'];
}
$https = $client->setAlwaysHttps($zoneId);
if (!$https['ok']) {
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
'cf_status' => 'error',
'cf_error' => 'Always HTTPS 失败: ' . ($https['error'] ?? ''),
'cf_checked_at' => date('Y-m-d H:i:s'),
]);
return ['ok' => false, 'message' => $https['error'] ?? 'Always HTTPS 配置失败'];
}
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
'cf_status' => 'ready',
'cf_error' => '',
'cf_checked_at' => date('Y-m-d H:i:s'),
]);
return [
'ok' => true,
'status' => 'ready',
'message' => '域名已生效:A 记录(@/www) 已指向 ' . $serverIp . ',已开启 Flexible SSL 与 Always HTTPS',
'nameservers' => $ns,
];
}
/**
* 删除本地记录并尝试删除 Cloudflare Zone
*
* @return array{ok:bool,message?:string,warning?:string}
*/
public static function deleteWithCloudflare(PDO $pdo, $domainId)
{
$row = DomainRepository::findById($pdo, $domainId);
if (!$row) {
return ['ok' => false, 'message' => '域名不存在'];
}
$warning = '';
$zoneId = trim((string) ($row['cf_zone_id'] ?? ''));
if ($zoneId !== '' && CloudflareConfig::isEnabled()) {
$del = self::newClient()->deleteZone($zoneId);
if (!$del['ok']) {
$warning = 'Cloudflare Zone 删除失败:' . ($del['error'] ?? '未知错误');
}
}
DomainRepository::deleteById($pdo, $domainId);
return [
'ok' => true,
'message' => '域名已删除',
'warning' => $warning,
];
}
}