Files
dnsmgr/app/controller/Cloudflare.php
wmwlwmwl 668e2b4ceb Cloudflare增强添加DCV 委派+优化,添加快速解析功能,已有解析记录和智能批量添加 (#442)
* Update RewriteRule in .htaccess for cleaner routing

修复Apache环境下路由重写规则
废弃旧版 index.php/$1 写法,改用兼容新版PHP的PATH_INFO传参方式
解决访问时报错 No input file specified. 问题

* Add files via upload

1.添加DCV 委派一键添加CNAME
2.添加证书验证方法和最低 TLS 版本
3.添加批量添加 修改 删除
4.修复华为云一键txt解析失败(我没其他dns, 其他的需关注)
5.Cloudflare增强改Cloudflare自定义主机名

* 1.添加快速解析 2.Cloudflare自定义主机名添加搜索功能

* Add files via upload

1.Cloudflare自定义主机名自动获取默认线路(支持所有dns,华为云退回之前)
2.优化手机上显示问题
3.一键添加 DCV 委派支持选择要写入的解析域名

* 优化手机显示

* 添加1. 批量 DCV 委派 2. 批量主机名 TXT 验证 3. 批量证书 TXT 验证 4. 批量刷新验证

1. 批量 DCV 委派
2. 批量主机名 TXT 验证
3. 批量证书 TXT 验证
4. 批量刷新验证

* 快速解析改名智能解析,添加已有解析记录和智能批量添加

* 快速解析改名智能解析,添加已有解析记录和智能批量添加

* 由于之前复制保存的,代码有些差异

* 修复已有解析记录的备注功能

* 备注按dns显示

* 修复记录值过长无法复制,优化显示

* 优化显示
2026-04-23 23:15:28 +08:00

1325 lines
57 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\controller;
use app\BaseController;
use app\lib\DnsHelper;
use app\service\CloudflareEnhanceService;
use Exception;
use think\facade\Db;
use think\facade\View;
class Cloudflare extends BaseController
{
public function hostnames()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
View::assign('domainId', $context['domain']['id']);
View::assign('domainName', $context['domain']['name']);
return view();
} catch (Exception $e) {
return $this->alert('error', $e->getMessage());
}
}
public function hostnames_data()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$rows = [];
foreach ($context['service']->listCustomHostnames($context['domain']['thirdid']) as $row) {
$rows[] = $this->formatCustomHostnameRow($row);
}
return json(['code' => 0, 'total' => count($rows), 'rows' => $rows]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage(), 'total' => 0, 'rows' => []]);
}
}
public function hostnames_add()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostname = trim(input('post.hostname', '', 'trim'));
$origin = trim(input('post.custom_origin_server', '', 'trim'));
$sslMethod = trim(input('post.ssl_method', 'txt', 'trim'));
$minTlsVersion = trim(input('post.min_tls_version', '1.0', 'trim'));
if (empty($hostname) || !checkDomain($hostname)) {
throw new Exception('主机名格式不正确');
}
if (!in_array($sslMethod, ['txt', 'http'])) {
throw new Exception('证书验证方法无效');
}
if (!in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
throw new Exception('最低 TLS 版本无效');
}
if ($origin !== '') {
$this->validateCustomOrigin($origin);
}
$result = $context['service']->createCustomHostname($context['domain']['thirdid'], $hostname, $origin !== '' ? $origin : null, $sslMethod, $minTlsVersion);
$this->add_log($context['domain']['name'], '创建自定义主机名', $hostname . ($origin !== '' ? ' -> ' . $origin : '') . ' (验证: ' . $sslMethod . ', TLS: ' . $minTlsVersion . ')');
return json(['code' => 0, 'msg' => '创建自定义主机名成功', 'data' => $this->formatCustomHostnameRow($result)]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function hostnames_update()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostnameId = trim(input('post.hostname_id', '', 'trim'));
if ($hostnameId === '') {
throw new Exception('缺少 hostname_id');
}
$current = $context['service']->getCustomHostname($context['domain']['thirdid'], $hostnameId);
$hostname = trim((string)($current['hostname'] ?? ''));
$origin = trim(input('post.custom_origin_server', '', 'trim'));
$sslMethod = trim(input('post.ssl_method', 'txt', 'trim'));
$minTlsVersion = trim(input('post.min_tls_version', '1.0', 'trim'));
if (!in_array($sslMethod, ['txt', 'http'])) {
throw new Exception('证书验证方法无效');
}
if (!in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
throw new Exception('最低 TLS 版本无效');
}
if ($origin !== '') {
$this->validateCustomOrigin($origin);
}
$result = $context['service']->updateCustomHostname(
$context['domain']['thirdid'],
$hostnameId,
[
'custom_origin_server' => $origin !== '' ? $origin : null,
'ssl' => $this->extractCustomHostnameSslPayload($current, $sslMethod, $minTlsVersion),
]
);
$this->add_log($context['domain']['name'], '编辑自定义主机名', $hostname . ' -> ' . ($origin !== '' ? $origin : '清空源站') . ' (验证: ' . $sslMethod . ', TLS: ' . $minTlsVersion . ')');
return json(['code' => 0, 'msg' => '更新自定义主机名成功', 'data' => $this->formatCustomHostnameRow($result)]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function hostnames_refresh()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostnameId = trim(input('post.hostname_id', '', 'trim'));
if ($hostnameId === '') {
throw new Exception('缺少 hostname_id');
}
$current = $context['service']->getCustomHostname($context['domain']['thirdid'], $hostnameId);
$hostname = trim((string)($current['hostname'] ?? $hostnameId));
$origin = trim((string)($current['custom_origin_server'] ?? ''));
$result = $context['service']->updateCustomHostname(
$context['domain']['thirdid'],
$hostnameId,
[
'custom_origin_server' => $origin !== '' ? $origin : null,
'ssl' => $this->extractCustomHostnameSslPayload($current),
]
);
$this->add_log($context['domain']['name'], '刷新自定义主机名验证', $hostname);
return json(['code' => 0, 'msg' => '已向 Cloudflare 重新发起验证', 'data' => $this->formatCustomHostnameRow($result)]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function hostnames_delete()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostnameId = trim(input('post.hostname_id', '', 'trim'));
$hostname = trim(input('post.hostname', '', 'trim'));
if ($hostnameId === '') {
throw new Exception('缺少 hostname_id');
}
$context['service']->deleteCustomHostname($context['domain']['thirdid'], $hostnameId);
$this->add_log($context['domain']['name'], '删除自定义主机名', $hostname);
return json(['code' => 0, 'msg' => '删除自定义主机名成功']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function hostnames_batch_delete()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostnameIds = input('post.hostname_ids/a', []);
if (empty($hostnameIds)) {
throw new Exception('缺少 hostname_ids');
}
$deletedCount = 0;
foreach ($hostnameIds as $hostnameId) {
if (trim((string)$hostnameId) !== '') {
try {
// 获取主机名信息用于日志
$hostnameInfo = $context['service']->getCustomHostname($context['domain']['thirdid'], trim((string)$hostnameId));
$hostname = trim((string)($hostnameInfo['hostname'] ?? ''));
$context['service']->deleteCustomHostname($context['domain']['thirdid'], trim((string)$hostnameId));
$deletedCount++;
// 为每个成功删除的主机名记录单独的日志
$this->add_log($context['domain']['name'], '批量删除自定义主机名', $hostname);
} catch (Exception $e) {
// 忽略删除失败的情况,继续处理其他主机名
}
}
}
return json(['code' => 0, 'msg' => '批量删除成功,共删除 ' . $deletedCount . ' 个自定义主机名']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function hostnames_batch_update()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostnameIds = input('post.hostname_ids/s', '');
$hostnameIdArray = array_filter(array_map('trim', explode(',', $hostnameIds)));
if (empty($hostnameIdArray)) {
throw new Exception('缺少 hostname_ids');
}
$origin = trim(input('post.custom_origin_server', '', 'trim'));
$sslMethod = trim(input('post.ssl_method', '', 'trim'));
$minTlsVersion = trim(input('post.min_tls_version', '', 'trim'));
if (!empty($sslMethod) && !in_array($sslMethod, ['txt', 'http'])) {
throw new Exception('证书验证方法无效');
}
if (!empty($minTlsVersion) && !in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
throw new Exception('最低 TLS 版本无效');
}
if ($origin !== '') {
$this->validateCustomOrigin($origin);
}
$updatedCount = 0;
foreach ($hostnameIdArray as $hostnameId) {
if (trim((string)$hostnameId) !== '') {
try {
$current = $context['service']->getCustomHostname($context['domain']['thirdid'], $hostnameId);
$hostname = trim((string)($current['hostname'] ?? ''));
$payload = [];
// 总是设置 custom_origin_server留空时设置为 null 表示清空
$payload['custom_origin_server'] = $origin !== '' ? $origin : null;
if (!empty($sslMethod) || !empty($minTlsVersion)) {
$payload['ssl'] = $this->extractCustomHostnameSslPayload($current, $sslMethod, $minTlsVersion);
}
if (!empty($payload)) {
$context['service']->updateCustomHostname($context['domain']['thirdid'], $hostnameId, $payload);
$updatedCount++;
// 为每个成功修改的主机名记录单独的日志
$logMessage = $hostname . ' -> ' . ($origin !== '' ? $origin : '清空源站') . ' (验证: ' . ($sslMethod ?: '保持不变') . ', TLS: ' . ($minTlsVersion ?: '保持不变') . ')';
$this->add_log($context['domain']['name'], '批量修改自定义主机名', $logMessage);
}
} catch (Exception $e) {
// 忽略修改失败的情况,继续处理其他主机名
}
}
}
return json(['code' => 0, 'msg' => '批量修改成功,共修改 ' . $updatedCount . ' 个自定义主机名']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function hostnames_batch_add()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostnamesText = trim(input('post.hostnames', '', 'trim'));
$origin = trim(input('post.custom_origin_server', '', 'trim'));
$sslMethod = trim(input('post.ssl_method', 'txt', 'trim'));
$minTlsVersion = trim(input('post.min_tls_version', '1.0', 'trim'));
if (empty($hostnamesText)) {
throw new Exception('缺少主机名列表');
}
if (!in_array($sslMethod, ['txt', 'http'])) {
throw new Exception('证书验证方法无效');
}
if (!in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
throw new Exception('最低 TLS 版本无效');
}
if ($origin !== '') {
$this->validateCustomOrigin($origin);
}
$hostnames = array_filter(array_map('trim', explode("\n", $hostnamesText)));
if (empty($hostnames)) {
throw new Exception('主机名列表为空');
}
$addedCount = 0;
$failedHostnames = [];
foreach ($hostnames as $hostname) {
if (empty($hostname)) {
continue;
}
if (!checkDomain($hostname)) {
$failedHostnames[] = $hostname . '(格式不正确)';
continue;
}
try {
$context['service']->createCustomHostname(
$context['domain']['thirdid'],
$hostname,
$origin !== '' ? $origin : null,
$sslMethod,
$minTlsVersion
);
$addedCount++;
// 为每个成功添加的主机名记录单独的日志
$logMessage = $hostname . ($origin !== '' ? ' -> ' . $origin : '') . ' (验证: ' . $sslMethod . ', TLS: ' . $minTlsVersion . ')';
$this->add_log($context['domain']['name'], '批量添加自定义主机名', $logMessage);
} catch (Exception $e) {
$failedHostnames[] = $hostname . '' . $e->getMessage() . '';
}
}
$message = '批量添加成功,共添加 ' . $addedCount . ' 个自定义主机名';
if (!empty($failedHostnames)) {
$message .= ',失败 ' . count($failedHostnames) . ' 个:' . implode('; ', $failedHostnames);
}
return json(['code' => 0, 'msg' => $message]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function hostnames_txt_targets()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostname = trim(input('post.hostname', '', 'trim'));
if ($hostname === '') {
throw new Exception('缺少 TXT 主机名');
}
return json([
'code' => 0,
'data' => [
'hostname' => $hostname,
'candidates' => $this->findTxtRecordTargetDomains($context['domain'], $hostname),
],
]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage(), 'data' => ['candidates' => []]]);
}
}
public function fallback_get()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$origin = $context['service']->getFallbackOrigin($context['domain']['thirdid']);
return json(['code' => 0, 'data' => ['origin' => $origin]]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function fallback_set()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$origin = trim(input('post.origin', '', 'trim'));
if ($origin === '') {
throw new Exception('Fallback Origin 不能为空');
}
$this->validateCustomOrigin($origin);
$savedOrigin = $context['service']->updateFallbackOrigin($context['domain']['thirdid'], $origin);
$this->add_log($context['domain']['name'], '更新 Fallback Origin', $savedOrigin);
return json(['code' => 0, 'msg' => '更新 Fallback Origin 成功', 'data' => ['origin' => $savedOrigin]]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function fallback_delete()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$context['service']->deleteFallbackOrigin($context['domain']['thirdid']);
$this->add_log($context['domain']['name'], '删除 Fallback Origin', '清空成功');
return json(['code' => 0, 'msg' => '已清空 Fallback Origin']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function dcv_delegation_uuid()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$uuid = $context['service']->getDcvDelegationUuid($context['domain']['thirdid']);
return json(['code' => 0, 'data' => ['uuid' => $uuid]]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function get_domain_default_line()
{
try {
$domainId = input('param.domain_id/d');
if (empty($domainId)) {
throw new Exception('缺少 domain_id 参数');
}
// 查询域名信息
$domainRow = Db::name('domain')->alias('A')
->join('account B', 'A.aid = B.id')
->where('A.id', $domainId)
->field('A.*, B.type, B.config account_config')
->find();
if (!$domainRow) {
throw new Exception('域名不存在');
}
// 获取该域名的默认线路
$recordLine = cache('record_line_' . $domainId);
if (empty($recordLine)) {
// 缓存中没有,需要从 DNS 提供商获取
$config = json_decode($domainRow['account_config'] ?? '', true);
if (!is_array($config)) {
$config = [];
}
$dnsModel = \app\lib\DnsHelper::getModel(
intval($domainRow['aid']),
$domainRow['name'],
$domainRow['thirdid'],
$domainRow['type'],
$config
);
if ($dnsModel && method_exists($dnsModel, 'getRecordLine')) {
$recordLine = $dnsModel->getRecordLine();
if ($recordLine && is_array($recordLine)) {
cache('record_line_' . $domainId, $recordLine, 604800); // 缓存7天
}
}
}
if (empty($recordLine) || !is_array($recordLine)) {
throw new Exception('无法获取该域名的解析线路列表');
}
$firstKey = array_key_first($recordLine);
if ($firstKey === null) {
throw new Exception('解析线路列表为空');
}
return json(['code' => 0, 'data' => ['default_line' => strval($firstKey)]]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function tunnels()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
View::assign('accountId', $context['account']['id']);
View::assign('accountName', $this->formatAccountDisplayName($context['account']));
View::assign('cfAccountId', $context['accountId']);
return view();
} catch (Exception $e) {
return $this->alert('error', $e->getMessage());
}
}
public function tunnels_data()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$rows = [];
foreach ($context['service']->listTunnels($context['accountId']) as $row) {
$rows[] = $this->formatTunnelRow($row);
}
return json(['code' => 0, 'total' => count($rows), 'rows' => $rows, 'account_id' => $context['accountId']]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage(), 'total' => 0, 'rows' => []]);
}
}
public function tunnels_add()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$name = trim(input('post.name', '', 'trim'));
if ($name === '') {
throw new Exception('Tunnel 名称不能为空');
}
$tunnel = $context['service']->createTunnel($context['accountId'], $name);
$this->add_log($this->formatAccountDisplayName($context['account']), '创建 Tunnel', $name . ' [' . ($tunnel['id'] ?? '-') . ']');
return json(['code' => 0, 'msg' => '创建 Tunnel 成功', 'data' => $this->formatTunnelRow($tunnel)]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function tunnels_delete()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$tunnelId = trim(input('post.tunnel_id', '', 'trim'));
if ($tunnelId === '') {
throw new Exception('缺少 tunnel_id');
}
$context['service']->deleteTunnel($context['accountId'], $tunnelId);
$this->add_log($this->formatAccountDisplayName($context['account']), '删除 Tunnel', $tunnelId);
return json(['code' => 0, 'msg' => '删除 Tunnel 成功']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function tunnels_token()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$tunnelId = trim(input('post.tunnel_id', '', 'trim'));
if ($tunnelId === '') {
throw new Exception('缺少 tunnel_id');
}
$token = $context['service']->getTunnelToken($context['accountId'], $tunnelId);
return json(['code' => 0, 'data' => ['token' => $token]]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function tunnels_public_hostnames_data()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$tunnelId = trim(input('post.tunnel_id', '', 'trim'));
if ($tunnelId === '') {
throw new Exception('缺少 tunnel_id');
}
$config = $this->extractTunnelConfigObject($context['service']->getTunnelConfig($context['accountId'], $tunnelId));
$rows = [];
foreach ($this->extractPublicHostnames($config) as $row) {
$zone = $this->findBestMatchingDomain(intval($context['account']['id']), $row['hostname']);
$row['zone_name'] = $zone['name'] ?? '';
$row['zone_id'] = $zone['thirdid'] ?? '';
$rows[] = $row;
}
return json(['code' => 0, 'total' => count($rows), 'rows' => $rows]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage(), 'total' => 0, 'rows' => []]);
}
}
public function tunnels_public_hostnames_save()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$tunnelId = trim(input('post.tunnel_id', '', 'trim'));
$hostname = trim(input('post.hostname', '', 'trim'));
$serviceValue = trim(input('post.service', '', 'trim'));
$path = trim(input('post.path', '', 'trim'));
if ($tunnelId === '' || $hostname === '' || $serviceValue === '') {
throw new Exception('Tunnel、主机名、服务地址不能为空');
}
if (!checkDomain($hostname)) {
throw new Exception('主机名格式不正确');
}
$zone = $this->findBestMatchingDomain(intval($context['account']['id']), $hostname);
if (empty($zone) || empty($zone['thirdid'])) {
throw new Exception('未找到匹配的本地域名,请先在当前 Cloudflare 账户下导入该主机名所属主域');
}
$config = $this->extractTunnelConfigObject($context['service']->getTunnelConfig($context['accountId'], $tunnelId));
$oldConfig = json_decode(json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true);
$ingress = isset($config['ingress']) && is_array($config['ingress']) ? array_values($config['ingress']) : [];
$rule = [
'hostname' => $hostname,
'service' => $serviceValue,
];
if ($path !== '') {
$rule['path'] = $path;
}
$existingIndex = $this->findPublicHostnameIndex($ingress, $hostname, $path);
if ($existingIndex >= 0) {
$next = array_merge($ingress[$existingIndex], $rule);
if ($path === '' && isset($next['path'])) {
unset($next['path']);
}
$ingress[$existingIndex] = $next;
} else {
$fallbackIndex = $this->findFallbackIngressIndex($ingress);
if ($fallbackIndex >= 0) {
array_splice($ingress, $fallbackIndex, 0, [$rule]);
} else {
$ingress[] = $rule;
}
}
$config['ingress'] = $this->ensureFallbackIngress($ingress);
$context['service']->updateTunnelConfig($context['accountId'], $tunnelId, $config);
try {
$dns = $context['service']->upsertTunnelCnameRecord($zone['thirdid'], $hostname, $tunnelId);
} catch (Exception $e) {
$context['service']->updateTunnelConfig($context['accountId'], $tunnelId, $oldConfig);
throw new Exception('Public Hostname 已回滚:' . $e->getMessage());
}
$this->add_log($this->formatAccountDisplayName($context['account']), '配置 Tunnel 公网主机名', $hostname . ' -> ' . $serviceValue . ' [' . ($dns['action'] ?? '-') . ']');
return json(['code' => 0, 'msg' => '配置 Public Hostname 成功']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function tunnels_public_hostnames_delete()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$tunnelId = trim(input('post.tunnel_id', '', 'trim'));
$hostname = trim(input('post.hostname', '', 'trim'));
$path = trim(input('post.path', '', 'trim'));
if ($tunnelId === '' || $hostname === '') {
throw new Exception('缺少 tunnel_id 或 hostname');
}
$config = $this->extractTunnelConfigObject($context['service']->getTunnelConfig($context['accountId'], $tunnelId));
$oldConfig = json_decode(json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true);
$ingress = isset($config['ingress']) && is_array($config['ingress']) ? array_values($config['ingress']) : [];
$nextIngress = [];
foreach ($ingress as $row) {
if (!is_array($row)) {
continue;
}
$match = $this->normalizeHostname($row['hostname'] ?? '') === $this->normalizeHostname($hostname)
&& trim((string)($row['path'] ?? '')) === $path;
if (!$match) {
$nextIngress[] = $row;
}
}
$config['ingress'] = $this->ensureFallbackIngress($nextIngress);
$context['service']->updateTunnelConfig($context['accountId'], $tunnelId, $config);
$zone = $this->findBestMatchingDomain(intval($context['account']['id']), $hostname);
if (!empty($zone['thirdid'])) {
try {
$context['service']->deleteTunnelCnameRecordIfMatch($zone['thirdid'], $hostname, $tunnelId);
} catch (Exception $e) {
$context['service']->updateTunnelConfig($context['accountId'], $tunnelId, $oldConfig);
throw new Exception('删除 Public Hostname 时已回滚:' . $e->getMessage());
}
}
$this->add_log($this->formatAccountDisplayName($context['account']), '删除 Tunnel 公网主机名', $hostname . ($path !== '' ? ' [' . $path . ']' : ''));
return json(['code' => 0, 'msg' => '删除 Public Hostname 成功']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function tunnels_cidr_data()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$tunnelId = trim(input('post.tunnel_id', '', 'trim'));
if ($tunnelId === '') {
throw new Exception('缺少 tunnel_id');
}
$rows = [];
foreach ($context['service']->listCidrRoutes($context['accountId'], $tunnelId) as $row) {
$mapped = $this->formatCidrRouteRow($row);
if ($mapped['id'] !== '' && $mapped['network'] !== '') {
$rows[] = $mapped;
}
}
return json(['code' => 0, 'total' => count($rows), 'rows' => $rows]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage(), 'total' => 0, 'rows' => []]);
}
}
public function tunnels_cidr_add()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$tunnelId = trim(input('post.tunnel_id', '', 'trim'));
$network = trim(input('post.network', '', 'trim'));
$comment = trim(input('post.comment', '', 'trim'));
if ($tunnelId === '' || $network === '') {
throw new Exception('Tunnel 和 CIDR 不能为空');
}
if (!$this->isValidCidr($network)) {
throw new Exception('CIDR 格式不正确');
}
$route = $context['service']->createCidrRoute($context['accountId'], $tunnelId, $network, $comment !== '' ? $comment : null);
$mapped = $this->formatCidrRouteRow($route);
$this->add_log($this->formatAccountDisplayName($context['account']), '创建 Tunnel CIDR 路由', $mapped['network']);
return json(['code' => 0, 'msg' => '创建 CIDR 路由成功', 'data' => $mapped]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function tunnels_cidr_delete()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$tunnelId = trim(input('post.tunnel_id', '', 'trim'));
$routeId = trim(input('post.route_id', '', 'trim'));
if ($tunnelId === '' || $routeId === '') {
throw new Exception('缺少 tunnel_id 或 route_id');
}
$matched = false;
foreach ($context['service']->listCidrRoutes($context['accountId'], $tunnelId) as $row) {
if (trim((string)($row['id'] ?? '')) === $routeId) {
$matched = true;
break;
}
}
if (!$matched) {
throw new Exception('CIDR 路由不存在或不属于当前 Tunnel');
}
$context['service']->deleteCidrRoute($context['accountId'], $routeId);
$this->add_log($this->formatAccountDisplayName($context['account']), '删除 Tunnel CIDR 路由', $routeId);
return json(['code' => 0, 'msg' => '删除 CIDR 路由成功']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function tunnels_hostname_routes_data()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$tunnelId = trim(input('post.tunnel_id', '', 'trim'));
if ($tunnelId === '') {
throw new Exception('缺少 tunnel_id');
}
$rows = [];
foreach ($context['service']->listHostnameRoutes($context['accountId'], $tunnelId) as $row) {
$mapped = $this->formatHostnameRouteRow($row);
if ($mapped['id'] !== '' && $mapped['hostname'] !== '') {
$rows[] = $mapped;
}
}
return json(['code' => 0, 'total' => count($rows), 'rows' => $rows]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage(), 'total' => 0, 'rows' => []]);
}
}
public function tunnels_hostname_routes_add()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$tunnelId = trim(input('post.tunnel_id', '', 'trim'));
$hostname = trim(input('post.hostname', '', 'trim'));
$comment = trim(input('post.comment', '', 'trim'));
if ($tunnelId === '' || $hostname === '') {
throw new Exception('Tunnel 和主机名不能为空');
}
if (!checkDomain($hostname)) {
throw new Exception('主机名格式不正确');
}
$route = $context['service']->createHostnameRoute($context['accountId'], $tunnelId, $hostname, $comment !== '' ? $comment : null);
$mapped = $this->formatHostnameRouteRow($route);
$this->add_log($this->formatAccountDisplayName($context['account']), '创建 Tunnel 主机名路由', $mapped['hostname']);
return json(['code' => 0, 'msg' => '创建主机名路由成功', 'data' => $mapped]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function tunnels_hostname_routes_delete()
{
try {
$context = $this->getCloudflareAccountContext(input('param.id/d'), true, true);
$tunnelId = trim(input('post.tunnel_id', '', 'trim'));
$routeId = trim(input('post.route_id', '', 'trim'));
if ($tunnelId === '' || $routeId === '') {
throw new Exception('缺少 tunnel_id 或 route_id');
}
$matched = false;
foreach ($context['service']->listHostnameRoutes($context['accountId'], $tunnelId) as $row) {
$id = trim((string)($row['id'] ?? $row['hostname_route_id'] ?? ''));
if ($id === $routeId) {
$matched = true;
break;
}
}
if (!$matched) {
throw new Exception('主机名路由不存在或不属于当前 Tunnel');
}
$context['service']->deleteHostnameRoute($context['accountId'], $routeId);
$this->add_log($this->formatAccountDisplayName($context['account']), '删除 Tunnel 主机名路由', $routeId);
return json(['code' => 0, 'msg' => '删除主机名路由成功']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
private function getCloudflareDomainContext(int $domainId): array
{
if (!checkPermission(2)) {
throw new Exception('无权限');
}
$row = Db::name('domain')->alias('A')
->join('account B', 'A.aid = B.id')
->where('A.id', $domainId)
->field('A.*,B.type,B.config account_config,B.name account_name,B.remark account_remark')
->find();
if (!$row) {
throw new Exception('域名不存在');
}
if (($row['type'] ?? '') !== 'cloudflare') {
throw new Exception('仅支持 Cloudflare 域名');
}
if (empty($row['thirdid'])) {
throw new Exception('当前域名缺少 Cloudflare Zone ID');
}
$config = json_decode($row['account_config'] ?? '', true);
if (!is_array($config)) {
$config = [];
}
return [
'domain' => $row,
'config' => $config,
'service' => new CloudflareEnhanceService($config),
];
}
private function getCloudflareAccountContext(int $accountId, bool $requireAccountId = false, bool $requireTunnelApiToken = false): array
{
if (!checkPermission(2)) {
throw new Exception('无权限');
}
$account = Db::name('account')->where('id', $accountId)->find();
if (!$account) {
throw new Exception('域名账户不存在');
}
if (($account['type'] ?? '') !== 'cloudflare') {
throw new Exception('仅支持 Cloudflare 账户');
}
$config = json_decode($account['config'] ?? '', true);
if (!is_array($config)) {
$config = [];
}
$service = new CloudflareEnhanceService($config);
if ($requireTunnelApiToken && !$service->isApiTokenAuth()) {
throw new Exception('Cloudflare Tunnels 仅支持 API 令牌认证,请将当前账户的认证方式切换为 API令牌');
}
$resolvedAccountId = trim((string)($config['account_id'] ?? ''));
if ($requireAccountId && $resolvedAccountId === '') {
$resolvedAccountId = $service->getDefaultAccountId();
if ($resolvedAccountId !== '') {
$config['account_id'] = $resolvedAccountId;
Db::name('account')->where('id', $account['id'])->update([
'config' => json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
$account['config'] = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$service = new CloudflareEnhanceService($config);
}
}
if ($requireAccountId && $resolvedAccountId === '') {
throw new Exception('当前 Cloudflare 账户缺少 Account ID且无法自动探测。请编辑账户并补充 Account ID 后重试');
}
return [
'account' => $account,
'config' => $config,
'service' => $service,
'accountId' => $resolvedAccountId,
];
}
private function validateCustomOrigin(string $origin): void
{
if (preg_match('/^https?:\/\//i', $origin)) {
throw new Exception('自定义源站不支持填写 http:// 或 https://');
}
if (str_contains($origin, '*')) {
throw new Exception('自定义源站不支持通配符');
}
if (str_contains($origin, '/')) {
throw new Exception('自定义源站格式不正确');
}
if (preg_match('/:\d+$/', $origin)) {
throw new Exception('自定义源站不支持端口');
}
if (filter_var($origin, FILTER_VALIDATE_IP)) {
throw new Exception('自定义源站不支持 IP 地址,请填写域名');
}
if (!checkDomain($origin)) {
throw new Exception('自定义源站格式不正确');
}
}
private function extractCustomHostnameSslPayload(array $row, string $sslMethod = '', string $minTlsVersion = ''): array
{
$ssl = isset($row['ssl']) && is_array($row['ssl']) ? $row['ssl'] : [];
$payload = [
'method' => $sslMethod !== '' ? $sslMethod : trim((string)($ssl['method'] ?? 'http')),
'type' => trim((string)($ssl['type'] ?? 'dv')),
];
if ($payload['method'] === '') {
$payload['method'] = 'http';
}
if ($payload['type'] === '') {
$payload['type'] = 'dv';
}
// 添加 TLS 版本设置
if ($minTlsVersion !== '') {
$payload['settings'] = [
'min_tls_version' => $minTlsVersion
];
} elseif (isset($ssl['settings']) && is_array($ssl['settings'])) {
$payload['settings'] = $ssl['settings'];
}
return $payload;
}
private function formatCustomHostnameRow(array $row): array
{
$ssl = isset($row['ssl']) && is_array($row['ssl']) ? $row['ssl'] : [];
$ownership = isset($row['ownership_verification']) && is_array($row['ownership_verification']) ? $row['ownership_verification'] : [];
$ownershipHttp = isset($row['ownership_verification_http']) && is_array($row['ownership_verification_http']) ? $row['ownership_verification_http'] : [];
$verificationStatus = trim((string)($ownership['http']['status'] ?? $ownership['txt']['status'] ?? $ownership['status'] ?? ''));
if ($verificationStatus === '' && (
trim((string)($ownership['name'] ?? '')) !== ''
|| trim((string)($ownership['value'] ?? '')) !== ''
|| trim((string)($ownershipHttp['http_url'] ?? '')) !== ''
|| trim((string)($ownershipHttp['http_body'] ?? '')) !== ''
)) {
$verificationStatus = 'pending';
}
$validationErrors = [];
if (!empty($row['verification_errors']) && is_array($row['verification_errors'])) {
foreach ($row['verification_errors'] as $item) {
$message = trim((string)($item['message'] ?? $item));
if ($message !== '') {
$validationErrors[] = $message;
}
}
}
if (!empty($ssl['validation_errors']) && is_array($ssl['validation_errors'])) {
foreach ($ssl['validation_errors'] as $item) {
$message = trim((string)($item['message'] ?? $item));
if ($message !== '') {
$validationErrors[] = $message;
}
}
}
$sslValidationRecords = [];
if (!empty($ssl['validation_records']) && is_array($ssl['validation_records'])) {
foreach ($ssl['validation_records'] as $item) {
if (!is_array($item)) {
continue;
}
$sslValidationRecords[] = [
'status' => trim((string)($item['status'] ?? '')),
'txt_name' => trim((string)($item['txt_name'] ?? '')),
'txt_value' => trim((string)($item['txt_value'] ?? '')),
'cname_name' => trim((string)($item['cname_name'] ?? '')),
'cname_target' => trim((string)($item['cname_target'] ?? '')),
'http_url' => trim((string)($item['http_url'] ?? '')),
'http_body' => trim((string)($item['http_body'] ?? '')),
'emails' => !empty($item['emails']) && is_array($item['emails']) ? array_values(array_filter(array_map('strval', $item['emails']))) : [],
];
}
}
if (empty($sslValidationRecords) && (
trim((string)($ssl['txt_name'] ?? '')) !== ''
|| trim((string)($ssl['txt_value'] ?? '')) !== ''
|| trim((string)($ssl['cname_name'] ?? '')) !== ''
|| trim((string)($ssl['cname_target'] ?? '')) !== ''
|| trim((string)($ssl['http_url'] ?? '')) !== ''
|| trim((string)($ssl['http_body'] ?? '')) !== ''
)) {
$sslValidationRecords[] = [
'status' => trim((string)($ssl['status'] ?? '')),
'txt_name' => trim((string)($ssl['txt_name'] ?? '')),
'txt_value' => trim((string)($ssl['txt_value'] ?? '')),
'cname_name' => trim((string)($ssl['cname_name'] ?? '')),
'cname_target' => trim((string)($ssl['cname_target'] ?? '')),
'http_url' => trim((string)($ssl['http_url'] ?? '')),
'http_body' => trim((string)($ssl['http_body'] ?? '')),
'emails' => [],
];
}
$sslValidationStatuses = [];
foreach ($sslValidationRecords as $item) {
$status = trim((string)($item['status'] ?? ''));
if ($status !== '') {
$sslValidationStatuses[] = $status;
}
}
$sslValidationStatuses = array_values(array_unique(array_filter($sslValidationStatuses)));
$sslValidationStatus = count($sslValidationStatuses) > 0 ? implode(' / ', $sslValidationStatuses) : trim((string)($ssl['status'] ?? ''));
if ($sslValidationStatus === '') {
$sslValidationStatus = '-';
}
return [
'id' => trim((string)($row['id'] ?? '')),
'hostname' => trim((string)($row['hostname'] ?? '')),
'custom_origin_server' => trim((string)($row['custom_origin_server'] ?? '')),
'status' => trim((string)($row['status'] ?? '')),
'ssl' => $ssl,
'ssl_status' => trim((string)($ssl['status'] ?? '')),
'ssl_method' => trim((string)($ssl['method'] ?? '')),
'ssl_min_tls_version' => trim((string)($ssl['settings']['min_tls_version'] ?? '')),
'ssl_type' => trim((string)($ssl['type'] ?? '')),
'ssl_validation_status' => $sslValidationStatus,
'verification_status' => $verificationStatus !== '' ? $verificationStatus : '-',
'created_on' => trim((string)($row['created_at'] ?? $row['created_on'] ?? '')),
'validation_errors' => implode(' | ', array_values(array_unique(array_filter($validationErrors)))),
'ownership_verification' => [
'type' => trim((string)($ownership['type'] ?? '')),
'name' => trim((string)($ownership['name'] ?? '')),
'value' => trim((string)($ownership['value'] ?? '')),
'status' => $verificationStatus !== '' ? $verificationStatus : '-',
],
'ownership_verification_http' => [
'http_url' => trim((string)($ownershipHttp['http_url'] ?? '')),
'http_body' => trim((string)($ownershipHttp['http_body'] ?? '')),
],
'ssl_validation_records' => $sslValidationRecords,
];
}
private function formatTunnelRow(array $row): array
{
$connections = isset($row['connections']) && is_array($row['connections']) ? array_values($row['connections']) : [];
return [
'id' => trim((string)($row['id'] ?? '')),
'name' => trim((string)($row['name'] ?? '')),
'status' => trim((string)($row['status'] ?? 'unknown')),
'created_at' => trim((string)($row['created_at'] ?? '')),
'deleted_at' => trim((string)($row['deleted_at'] ?? '')),
'conns_active_at' => trim((string)($row['conns_active_at'] ?? '')),
'connection_count' => count($connections),
'connections' => $connections,
];
}
private function formatCidrRouteRow(array $row): array
{
return [
'id' => trim((string)($row['id'] ?? '')),
'network' => trim((string)($row['network'] ?? '')),
'comment' => trim((string)($row['comment'] ?? '')),
'virtual_network_id' => trim((string)($row['virtual_network_id'] ?? '')),
'tunnel_id' => trim((string)($row['tunnel_id'] ?? '')),
'created_at' => trim((string)($row['created_at'] ?? '')),
];
}
private function formatHostnameRouteRow(array $row): array
{
return [
'id' => trim((string)($row['id'] ?? $row['hostname_route_id'] ?? '')),
'hostname' => trim((string)($row['hostname'] ?? $row['hostname_pattern'] ?? '')),
'comment' => trim((string)($row['comment'] ?? '')),
'tunnel_id' => trim((string)($row['tunnel_id'] ?? '')),
'created_at' => trim((string)($row['created_at'] ?? '')),
];
}
private function findTxtRecordTargetDomains(array $currentDomain, string $hostname): array
{
$rows = Db::name('domain')->alias('D')
->join('account A', 'D.aid = A.id')
->field('D.id,D.aid,D.name,A.type account_type,A.name account_name,A.remark account_remark')
->select()
->toArray();
$candidates = [];
$bestLength = -1;
foreach ($rows as $row) {
$recordName = $this->matchHostnameToDomainRecordName($hostname, $row['name'] ?? '');
if ($recordName === null) {
continue;
}
$domainName = $this->normalizeHostname($row['name'] ?? '');
$matchedLength = strlen($domainName);
if ($matchedLength > $bestLength) {
$bestLength = $matchedLength;
$candidates = [];
}
if ($matchedLength === $bestLength) {
$candidates[] = $this->formatTxtTargetCandidate($row, $recordName, intval($currentDomain['id'] ?? 0));
}
}
if (empty($candidates)) {
$fallbackRecordName = $this->matchHostnameToDomainRecordName($hostname, $currentDomain['name'] ?? '', true);
if ($fallbackRecordName !== null) {
$candidates[] = $this->formatTxtTargetCandidate([
'id' => $currentDomain['id'] ?? 0,
'aid' => $currentDomain['aid'] ?? 0,
'name' => $currentDomain['name'] ?? '',
'account_type' => $currentDomain['type'] ?? '',
'account_name' => $currentDomain['account_name'] ?? '',
'account_remark' => $currentDomain['account_remark'] ?? '',
], $fallbackRecordName, intval($currentDomain['id'] ?? 0));
}
}
usort($candidates, function ($a, $b) {
if ($a['is_current_domain'] !== $b['is_current_domain']) {
return $a['is_current_domain'] ? -1 : 1;
}
$providerCompare = strcmp($a['account_type_name'], $b['account_type_name']);
if ($providerCompare !== 0) {
return $providerCompare;
}
$accountCompare = strcmp($a['account_display_name'], $b['account_display_name']);
if ($accountCompare !== 0) {
return $accountCompare;
}
return strcmp($a['domain_name'], $b['domain_name']);
});
return $candidates;
}
private function formatTxtTargetCandidate(array $row, string $recordName, int $currentDomainId): array
{
$account = [
'id' => intval($row['aid'] ?? 0),
'name' => trim((string)($row['account_name'] ?? '')),
'remark' => trim((string)($row['account_remark'] ?? '')),
];
$accountType = trim((string)($row['account_type'] ?? ''));
return [
'domain_id' => intval($row['id'] ?? 0),
'domain_name' => trim((string)($row['name'] ?? '')),
'record_name' => $recordName,
'account_id' => $account['id'],
'account_type' => $accountType,
'account_type_name' => $this->formatDnsTypeName($accountType),
'account_display_name' => $this->formatAccountDisplayName($account),
'is_current_domain' => intval($row['id'] ?? 0) === $currentDomainId,
];
}
private function formatAccountDisplayName(array $account): string
{
$name = trim((string)($account['name'] ?? ''));
$remark = trim((string)($account['remark'] ?? ''));
if ($remark !== '') {
return $remark . ' (' . $name . ')';
}
return $name !== '' ? $name : ('Cloudflare账户#' . ($account['id'] ?? ''));
}
private function extractTunnelConfigObject(array $raw): array
{
if (isset($raw['config']) && is_array($raw['config'])) {
return $raw['config'];
}
return $raw;
}
private function extractPublicHostnames(array $config): array
{
$rows = [];
$ingress = isset($config['ingress']) && is_array($config['ingress']) ? array_values($config['ingress']) : [];
foreach ($ingress as $rule) {
if (!is_array($rule)) {
continue;
}
$hostname = trim((string)($rule['hostname'] ?? ''));
if ($hostname === '') {
continue;
}
$rows[] = [
'hostname' => $hostname,
'path' => trim((string)($rule['path'] ?? '')),
'service' => trim((string)($rule['service'] ?? '')),
];
}
return $rows;
}
private function ensureFallbackIngress(array $ingress): array
{
$rows = [];
foreach ($ingress as $rule) {
if (is_array($rule)) {
$rows[] = $rule;
}
}
if (empty($rows) || !$this->isFallbackIngressRule($rows[count($rows) - 1])) {
$rows[] = ['service' => 'http_status:404'];
}
return $rows;
}
private function isFallbackIngressRule(array $rule): bool
{
return trim((string)($rule['hostname'] ?? '')) === '' && trim((string)($rule['path'] ?? '')) === '';
}
private function findFallbackIngressIndex(array $ingress): int
{
foreach ($ingress as $index => $rule) {
if (is_array($rule) && $this->isFallbackIngressRule($rule)) {
return $index;
}
}
return -1;
}
private function findPublicHostnameIndex(array $ingress, string $hostname, string $path): int
{
foreach ($ingress as $index => $rule) {
if (!is_array($rule)) {
continue;
}
$sameHostname = $this->normalizeHostname($rule['hostname'] ?? '') === $this->normalizeHostname($hostname);
$samePath = trim((string)($rule['path'] ?? '')) === trim($path);
if ($sameHostname && $samePath) {
return $index;
}
}
return -1;
}
private function findBestMatchingDomain(int $accountId, string $hostname): ?array
{
$hostname = preg_replace('/^\*\./', '', $this->normalizeHostname($hostname));
$domains = Db::name('domain')->where('aid', $accountId)->select()->toArray();
$best = null;
$bestLength = -1;
foreach ($domains as $domain) {
$domainName = $this->normalizeHostname($domain['name'] ?? '');
if ($domainName === '') {
continue;
}
if ($this->matchHostnameToDomainRecordName($hostname, $domainName) !== null && strlen($domainName) > $bestLength) {
$best = $domain;
$bestLength = strlen($domainName);
}
}
return $best;
}
private function matchHostnameToDomainRecordName(string $hostname, string $domainName, bool $allowRelative = false): ?string
{
$hostname = preg_replace('/^\*\./', '', $this->normalizeHostname($hostname));
$domainName = $this->normalizeHostname($domainName);
if ($hostname === '' || $domainName === '') {
return null;
}
if ($hostname === $domainName) {
return '@';
}
if (str_ends_with($hostname, '.' . $domainName)) {
return substr($hostname, 0, -strlen($domainName) - 1);
}
if ($allowRelative) {
if ($hostname === '@') {
return '@';
}
if (!str_contains($hostname, '.')) {
return $hostname;
}
}
return null;
}
private function formatDnsTypeName(string $type): string
{
$dnsList = DnsHelper::getList();
return $dnsList[$type]['name'] ?? ($type !== '' ? $type : '-');
}
private function normalizeHostname($hostname): string
{
$hostname = trim((string)$hostname);
if ($hostname === '') {
return '';
}
$hostname = convertDomainToAscii(rtrim($hostname, '.'));
return strtolower($hostname);
}
private function isValidCidr(string $network): bool
{
if (!str_contains($network, '/')) {
return false;
}
[$ip, $prefix] = explode('/', $network, 2);
if (!is_numeric($prefix)) {
return false;
}
$prefix = intval($prefix);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $prefix >= 0 && $prefix <= 32;
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return $prefix >= 0 && $prefix <= 128;
}
return false;
}
private function add_log(string $domain, string $action, string $data): void
{
if (strlen($data) > 500) {
$data = substr($data, 0, 500);
}
Db::name('log')->insert([
'uid' => request()->user['id'],
'domain' => $domain,
'action' => $action,
'data' => $data,
'addtime' => date('Y-m-d H:i:s'),
]);
}
}