From 668e2b4cebcef602304991513b36923c29aefa95 Mon Sep 17 00:00:00 2001 From: wmwlwmwl <168271477+wmwlwmwl@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:15:28 +0800 Subject: [PATCH] =?UTF-8?q?Cloudflare=E5=A2=9E=E5=BC=BA=E6=B7=BB=E5=8A=A0D?= =?UTF-8?q?CV=20=E5=A7=94=E6=B4=BE+=E4=BC=98=E5=8C=96=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=BF=AB=E9=80=9F=E8=A7=A3=E6=9E=90=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=B7=B2=E6=9C=89=E8=A7=A3=E6=9E=90=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=92=8C=E6=99=BA=E8=83=BD=E6=89=B9=E9=87=8F=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20(#442)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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显示 * 修复记录值过长无法复制,优化显示 * 优化显示 --- app/controller/Cloudflare.php | 274 ++- app/controller/Domain.php | 62 + app/service/CloudflareEnhanceService.php | 20 +- app/view/cloudflare/hostnames.html | 2068 +++++++++++++++++++- app/view/common/layout.html | 5 +- app/view/domain/record.html | 2 +- app/view/domain/smartparse.html | 2263 ++++++++++++++++++++++ public/.htaccess | 2 +- route/app.php | 9 +- 9 files changed, 4584 insertions(+), 121 deletions(-) create mode 100644 app/view/domain/smartparse.html diff --git a/app/controller/Cloudflare.php b/app/controller/Cloudflare.php index 1843136..4f2b8dd 100644 --- a/app/controller/Cloudflare.php +++ b/app/controller/Cloudflare.php @@ -43,15 +43,23 @@ class Cloudflare extends BaseController $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); - $this->add_log($context['domain']['name'], '创建自定义主机名', $hostname . ($origin !== '' ? ' -> ' . $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()]); @@ -70,6 +78,14 @@ class Cloudflare extends BaseController $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); } @@ -79,10 +95,10 @@ class Cloudflare extends BaseController $hostnameId, [ 'custom_origin_server' => $origin !== '' ? $origin : null, - 'ssl' => $this->extractCustomHostnameSslPayload($current), + 'ssl' => $this->extractCustomHostnameSslPayload($current, $sslMethod, $minTlsVersion), ] ); - $this->add_log($context['domain']['name'], '编辑自定义主机名', $hostname . ' -> ' . ($origin !== '' ? $origin : '清空源站')); + $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()]); @@ -109,8 +125,8 @@ class Cloudflare extends BaseController 'ssl' => $this->extractCustomHostnameSslPayload($current), ] ); - $this->add_log($context['domain']['name'], '刷新自定义主机名校验', $hostname); - return json(['code' => 0, 'msg' => '已向 Cloudflare 重新发起校验', 'data' => $this->formatCustomHostnameRow($result)]); + $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()]); } @@ -125,15 +141,170 @@ class Cloudflare extends BaseController if ($hostnameId === '') { throw new Exception('缺少 hostname_id'); } - $context['service']->deleteCustomHostname($context['domain']['thirdid'], $hostnameId); - $this->add_log($context['domain']['name'], '删除自定义主机名', $hostname !== '' ? $hostname : $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 { @@ -196,6 +367,77 @@ class Cloudflare extends BaseController } } + 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 { @@ -650,11 +892,11 @@ class Cloudflare extends BaseController } } - private function extractCustomHostnameSslPayload(array $row): array + private function extractCustomHostnameSslPayload(array $row, string $sslMethod = '', string $minTlsVersion = ''): array { $ssl = isset($row['ssl']) && is_array($row['ssl']) ? $row['ssl'] : []; $payload = [ - 'method' => trim((string)($ssl['method'] ?? 'http')), + 'method' => $sslMethod !== '' ? $sslMethod : trim((string)($ssl['method'] ?? 'http')), 'type' => trim((string)($ssl['type'] ?? 'dv')), ]; if ($payload['method'] === '') { @@ -663,6 +905,16 @@ class Cloudflare extends BaseController 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; } @@ -755,8 +1007,10 @@ class Cloudflare extends BaseController '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 : '-', diff --git a/app/controller/Domain.php b/app/controller/Domain.php index c930df8..ac45999 100644 --- a/app/controller/Domain.php +++ b/app/controller/Domain.php @@ -1005,6 +1005,68 @@ class Domain extends BaseController return view('log'); } + public function smartparse() + { + if (request()->user['type'] == 'domain') { + return redirect('/record/' . request()->user['id']); + } + + $list = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id') + ->field('A.id, A.name, A.aid, B.type') + ->order('A.name', 'asc') + ->select(); + + $domainList = []; + foreach ($list as $row) { + if (request()->user['level'] == 1 && !in_array($row['name'], request()->user['permission'])) { + continue; + } + $dnsTypeName = isset(DnsHelper::$dns_config[$row['type']]) ? DnsHelper::$dns_config[$row['type']]['name'] : $row['type']; + $domainList[] = [ + 'id' => $row['id'], + 'name' => $row['name'], + 'dnsType' => $dnsTypeName + ]; + } + + View::assign('domainList', $domainList); + return view(); + } + + public function quickinfo() + { + $id = input('param.id/d'); + $drow = Db::name('domain')->where('id', $id)->find(); + if (!$drow) { + return json(['code' => -1, 'msg' => '域名不存在']); + } + if (!checkPermission(0, $drow['name'])) return json(['code' => -1, 'msg' => '无权限']); + + try { + list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); + + $recordLineArr = []; + foreach ($recordLine as $key => $item) { + $recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']]; + } + + $dnstype = Db::name('account')->where('id', $drow['aid'])->value('type'); + $dnsconfig = DnsHelper::$dns_config[$dnstype]; + + return json([ + 'code' => 0, + 'data' => [ + 'recordLine' => $recordLineArr, + 'minTTL' => $minTTL ? $minTTL : 1, + 'weight' => $dnsconfig['weight'] ?? false, + 'remark' => $dnsconfig['remark'] ?? 0 + ] + ]); + } catch (Exception $e) { + return json(['code' => -1, 'msg' => $e->getMessage()]); + } + } + private function add_log($domain, $action, $data) { if (strlen($data) > 500) $data = substr($data, 0, 500); diff --git a/app/service/CloudflareEnhanceService.php b/app/service/CloudflareEnhanceService.php index 66123ac..10d487b 100644 --- a/app/service/CloudflareEnhanceService.php +++ b/app/service/CloudflareEnhanceService.php @@ -91,14 +91,17 @@ class CloudflareEnhanceService } } - public function createCustomHostname(string $zoneId, string $hostname, ?string $customOriginServer = null): array + public function createCustomHostname(string $zoneId, string $hostname, ?string $customOriginServer = null, string $sslMethod = 'http', string $minTlsVersion = '1.0'): array { $hostname = $this->normalizeHostname($hostname); $payload = [ 'hostname' => $hostname, 'ssl' => [ - 'method' => 'http', + 'method' => $sslMethod === 'txt' ? 'txt' : 'http', 'type' => 'dv', + 'settings' => [ + 'min_tls_version' => $minTlsVersion + ] ], ]; $origin = trim((string)$customOriginServer); @@ -180,6 +183,19 @@ class CloudflareEnhanceService } } + public function getDcvDelegationUuid(string $zoneId): string + { + try { + $result = $this->requestResult('GET', '/zones/' . $zoneId . '/dcv_delegation/uuid', [], null, true); + if ($result === null) { + return ''; + } + return trim((string)($result['uuid'] ?? '')); + } catch (Exception $e) { + $this->throwActionError('获取 DCV 委派 UUID', $e, 'SSL and Certificates:Read'); + } + } + public function listTunnels(string $accountId): array { $this->assertTunnelSupported(); diff --git a/app/view/cloudflare/hostnames.html b/app/view/cloudflare/hostnames.html index e3fef57..ae4cf33 100644 --- a/app/view/cloudflare/hostnames.html +++ b/app/view/cloudflare/hostnames.html @@ -1,42 +1,70 @@ {extend name="common/layout" /} -{block name="title"}Cloudflare增强 - {$domainName}{/block} +{block name="title"}Cloudflare自定义主机名 - {$domainName}{/block} {block name="main"}