mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-05-02 11:56:27 +02:00
```
feat(cloudflare): 添加自定义主机名编辑和刷新验证功能 - 新增 hostnames_update 方法用于更新自定义主机名的自定义源站配置 - 新增 hostnames_refresh 方法用于重新向 Cloudflare 发起验证请求 - 添加 extractCustomHostnameSslPayload 辅助方法处理 SSL 配置参数 - 完善 formatCustomHostnameRow 方法,增加更详细的验证状态信息展示 - 在前端界面添加编辑按钮和校验对话框,支持在线查看和刷新验证记录 - 优化自定义主机名列表页面,支持实时更新和状态显示 - 新增证书校验和所有权验证的详细信息展示界面 ```
This commit is contained in:
144
.codex-tmp/cloudflare_hostname_edit_smoke.php
Normal file
144
.codex-tmp/cloudflare_hostname_edit_smoke.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
function convertDomainToAscii($domain)
|
||||
{
|
||||
return (string)$domain;
|
||||
}
|
||||
|
||||
function http_request($url, $data = null, $referer = null, $cookie = null, $headers = null, $proxy = false, $method = null, $timeout = 10): array
|
||||
{
|
||||
$method = strtoupper((string)($method ?: ($data !== null ? 'POST' : 'GET')));
|
||||
$headerLines = [
|
||||
'User-Agent: Codex-Hostname-Smoke/1.0',
|
||||
];
|
||||
$normalizedHeaders = [];
|
||||
foreach ((array)$headers as $key => $value) {
|
||||
$normalizedHeaders[strtolower((string)$key)] = (string)$value;
|
||||
$headerLines[] = $key . ': ' . $value;
|
||||
}
|
||||
|
||||
$content = null;
|
||||
if ($data !== null && $method !== 'GET') {
|
||||
if (is_array($data) || is_object($data)) {
|
||||
$contentType = $normalizedHeaders['content-type'] ?? 'application/x-www-form-urlencoded';
|
||||
if (stripos($contentType, 'application/json') !== false) {
|
||||
$content = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} else {
|
||||
$content = http_build_query((array)$data);
|
||||
}
|
||||
} else {
|
||||
$content = (string)$data;
|
||||
}
|
||||
}
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => $method,
|
||||
'header' => implode("\r\n", $headerLines),
|
||||
'content' => $content,
|
||||
'timeout' => $timeout,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$body = @file_get_contents($url, false, $context);
|
||||
if ($body === false) {
|
||||
$body = '';
|
||||
}
|
||||
$responseHeaders = $http_response_header ?? [];
|
||||
$statusLine = $responseHeaders[0] ?? '';
|
||||
$statusCode = preg_match('#\s(\d{3})\s#', $statusLine, $match) ? intval($match[1]) : 0;
|
||||
|
||||
return [
|
||||
'code' => $statusCode,
|
||||
'headers' => $responseHeaders,
|
||||
'body' => $body,
|
||||
];
|
||||
}
|
||||
|
||||
require __DIR__ . '/../app/service/CloudflareEnhanceService.php';
|
||||
|
||||
use app\service\CloudflareEnhanceService;
|
||||
|
||||
$token = getenv('CF_API_TOKEN') ?: '';
|
||||
$zoneId = getenv('CF_ZONE_ID') ?: '';
|
||||
$zoneName = getenv('CF_ZONE_NAME') ?: '';
|
||||
|
||||
if ($token === '' || $zoneId === '' || $zoneName === '') {
|
||||
fwrite(STDERR, "Missing CF_API_TOKEN / CF_ZONE_ID / CF_ZONE_NAME\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$service = new CloudflareEnhanceService([
|
||||
'apikey' => $token,
|
||||
'auth' => '1',
|
||||
]);
|
||||
|
||||
$prefix = 'codex-edit-' . time();
|
||||
$hostname = $prefix . '.' . $zoneName;
|
||||
$origin = 'origin-' . $prefix . '.' . $zoneName;
|
||||
|
||||
$summary = [
|
||||
'created' => null,
|
||||
'updated' => null,
|
||||
'refreshed' => null,
|
||||
'cleanup' => null,
|
||||
];
|
||||
|
||||
$created = null;
|
||||
|
||||
try {
|
||||
$created = $service->createCustomHostname($zoneId, $hostname, null);
|
||||
$hostnameId = (string)($created['id'] ?? '');
|
||||
$summary['created'] = [
|
||||
'id' => $hostnameId,
|
||||
'hostname' => $created['hostname'] ?? '',
|
||||
'ownership_txt_name' => $created['ownership_verification']['name'] ?? '',
|
||||
'ownership_txt_value' => $created['ownership_verification']['value'] ?? '',
|
||||
'ownership_http_url' => $created['ownership_verification_http']['http_url'] ?? '',
|
||||
'ownership_http_body' => $created['ownership_verification_http']['http_body'] ?? '',
|
||||
];
|
||||
|
||||
$updated = $service->updateCustomHostname($zoneId, $hostnameId, [
|
||||
'custom_origin_server' => $origin,
|
||||
'ssl' => [
|
||||
'method' => 'http',
|
||||
'type' => 'dv',
|
||||
],
|
||||
]);
|
||||
$summary['updated'] = [
|
||||
'custom_origin_server' => $updated['custom_origin_server'] ?? '',
|
||||
'ssl_status' => $updated['ssl']['status'] ?? '',
|
||||
'validation_record_count' => count($updated['ssl']['validation_records'] ?? []),
|
||||
'first_http_url' => $updated['ssl']['validation_records'][0]['http_url'] ?? ($updated['ssl']['http_url'] ?? ''),
|
||||
'first_http_body' => $updated['ssl']['validation_records'][0]['http_body'] ?? ($updated['ssl']['http_body'] ?? ''),
|
||||
];
|
||||
|
||||
$current = $service->getCustomHostname($zoneId, $hostnameId);
|
||||
$refreshed = $service->updateCustomHostname($zoneId, $hostnameId, [
|
||||
'custom_origin_server' => trim((string)($current['custom_origin_server'] ?? '')) !== '' ? $current['custom_origin_server'] : null,
|
||||
'ssl' => [
|
||||
'method' => $current['ssl']['method'] ?? 'http',
|
||||
'type' => $current['ssl']['type'] ?? 'dv',
|
||||
],
|
||||
]);
|
||||
$summary['refreshed'] = [
|
||||
'ssl_status' => $refreshed['ssl']['status'] ?? '',
|
||||
'validation_record_count' => count($refreshed['ssl']['validation_records'] ?? []),
|
||||
'ownership_txt_name' => $refreshed['ownership_verification']['name'] ?? '',
|
||||
'ownership_http_url' => $refreshed['ownership_verification_http']['http_url'] ?? '',
|
||||
];
|
||||
} finally {
|
||||
if ($created && !empty($created['id'])) {
|
||||
try {
|
||||
$service->deleteCustomHostname($zoneId, (string)$created['id']);
|
||||
$summary['cleanup'] = true;
|
||||
} catch (Throwable $e) {
|
||||
$summary['cleanup'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($summary, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), PHP_EOL;
|
||||
420
.codex-tmp/hostnames.page.check.js
Normal file
420
.codex-tmp/hostnames.page.check.js
Normal file
@@ -0,0 +1,420 @@
|
||||
var currentVerificationHostnameId = '';
|
||||
|
||||
$(document).ready(function(){
|
||||
$("#form-store").bootstrapValidator();
|
||||
loadFallbackOrigin();
|
||||
$("#listTable").bootstrapTable({
|
||||
url: '/cloudflare/hostnames/data/1',
|
||||
method: 'post',
|
||||
toolbar: '',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
responseHandler: hostnameResponseHandler,
|
||||
columns: [
|
||||
{field: 'hostname', title: '主机名'},
|
||||
{field: 'custom_origin_server', title: '自定义源站', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'ssl_status', title: '证书状态', formatter: formatStatus},
|
||||
{field: 'ssl_validation_status', title: '证书校验', formatter: formatStatus},
|
||||
{field: 'verification_status', title: '所有权校验', formatter: formatStatus},
|
||||
{field: 'created_on', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'validation_errors', title: '错误信息', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return ''
|
||||
+ '<a href="javascript:openEditDialog(\''+row.id+'\')" class="btn btn-info btn-xs">编辑</a> '
|
||||
+ '<a href="javascript:openVerificationDialog(\''+row.id+'\')" class="btn btn-primary btn-xs">校验</a> '
|
||||
+ '<a href="javascript:deleteHostname(\''+row.id+'\', \''+htmlEscape(row.hostname)+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
function hostnameResponseHandler(res){
|
||||
if(res.code !== 0){
|
||||
layer.alert(res.msg || '获取自定义主机名失败', {icon: 2});
|
||||
return {total: 0, rows: []};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function refreshHostnameList(){
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
function formatStatus(value){
|
||||
var v = String(value || '').toLowerCase();
|
||||
if(v === 'active' || v === 'active_deployed' || v === 'valid'){
|
||||
return '<span class="label label-success">'+htmlEscape(value)+'</span>';
|
||||
}
|
||||
if(v === 'pending' || v === 'pending_validation' || v === 'initializing' || v === 'in_progress'){
|
||||
return '<span class="label label-warning">'+htmlEscape(value || '-')+'</span>';
|
||||
}
|
||||
if(v && v !== '-'){
|
||||
return '<span class="label label-danger">'+htmlEscape(value)+'</span>';
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
function getHostnameRow(id){
|
||||
var row = $("#listTable").bootstrapTable('getRowByUniqueId', id);
|
||||
if(!row){
|
||||
layer.alert('未找到自定义主机名数据,请先刷新列表后重试', {icon: 2});
|
||||
return null;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function resetHostnameForm(){
|
||||
$("#form-store")[0].reset();
|
||||
$("#form-store input[name=hostname_id]").val('');
|
||||
$("#form-store input[name=hostname]").prop('readonly', false);
|
||||
$("#form-store").data("bootstrapValidator").resetForm(true);
|
||||
}
|
||||
|
||||
function openAddDialog(){
|
||||
resetHostnameForm();
|
||||
$("#storeTitle").text('添加自定义主机名');
|
||||
$("#hostnameHint").text('创建后主机名不能直接改名,如需改名请删除后重建。');
|
||||
$("#modal-store").modal('show');
|
||||
}
|
||||
|
||||
function openEditDialog(id){
|
||||
var row = getHostnameRow(id);
|
||||
if(!row){
|
||||
return;
|
||||
}
|
||||
resetHostnameForm();
|
||||
$("#storeTitle").text('编辑自定义主机名');
|
||||
$("#hostnameHint").text('主机名不可直接改名,当前仅支持修改或清空自定义源站。');
|
||||
$("#form-store input[name=hostname_id]").val(row.id);
|
||||
$("#form-store input[name=hostname]").val(row.hostname).prop('readonly', true);
|
||||
$("#form-store input[name=custom_origin_server]").val(row.custom_origin_server || '');
|
||||
$("#modal-store").modal('show');
|
||||
}
|
||||
|
||||
function submitHostname(){
|
||||
$("#form-store").data("bootstrapValidator").validate();
|
||||
if(!$("#form-store").data("bootstrapValidator").isValid()){
|
||||
return;
|
||||
}
|
||||
var hostnameId = $.trim($("#form-store input[name=hostname_id]").val());
|
||||
var url = hostnameId ? '/cloudflare/hostnames/update/1' : '/cloudflare/hostnames/add/1';
|
||||
var successMsg = hostnameId ? '更新自定义主机名成功' : '创建自定义主机名成功';
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: $("#form-store").serialize(),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
$("#modal-store").modal('hide');
|
||||
layer.msg(res.msg || successMsg, {icon: 1, time: 1200});
|
||||
if(res.data && res.data.id){
|
||||
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
||||
if(!$("#listTable").bootstrapTable('getRowByUniqueId', res.data.id)){
|
||||
refreshHostnameList();
|
||||
}
|
||||
}else{
|
||||
refreshHostnameList();
|
||||
}
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openVerificationDialog(id){
|
||||
var row = getHostnameRow(id);
|
||||
if(!row){
|
||||
return;
|
||||
}
|
||||
currentVerificationHostnameId = id;
|
||||
renderVerificationDialog(row);
|
||||
$("#modal-verification").modal('show');
|
||||
}
|
||||
|
||||
function refreshHostnameValidation(){
|
||||
if(!currentVerificationHostnameId){
|
||||
layer.msg('请先选择自定义主机名');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/hostnames/refresh/1',
|
||||
data: {hostname_id: currentVerificationHostnameId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
if(res.data && res.data.id){
|
||||
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
||||
renderVerificationDialog(res.data);
|
||||
}else{
|
||||
refreshHostnameList();
|
||||
}
|
||||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderVerificationDialog(row){
|
||||
$("#verificationTitle").text('证书校验 - ' + row.hostname);
|
||||
var html = '';
|
||||
html += '<div class="alert alert-info"><strong>说明:</strong> 下列值直接来自 Cloudflare 返回结果,可直接复制到 DNS、源站或验证目录中。点击“刷新校验”会重新向 Cloudflare 发起一次校验。</div>';
|
||||
html += '<div class="row">';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('证书状态', formatStatusText(row.ssl_status))+'</div>';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('证书校验', formatStatusText(row.ssl_validation_status))+'</div>';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('所有权校验', formatStatusText(row.verification_status))+'</div>';
|
||||
html += '</div>';
|
||||
|
||||
var ownership = row.ownership_verification || {};
|
||||
if(ownership.name || ownership.value){
|
||||
html += renderSection('所有权 TXT 校验',
|
||||
renderCopyInput('记录类型', ownership.type || 'txt', false)
|
||||
+ renderCopyInput('TXT 名称', ownership.name || '', true)
|
||||
+ renderCopyTextarea('TXT 值', ownership.value || '', true, 3)
|
||||
);
|
||||
}
|
||||
|
||||
var ownershipHttp = row.ownership_verification_http || {};
|
||||
if(ownershipHttp.http_url || ownershipHttp.http_body){
|
||||
html += renderSection('所有权 HTTP 校验',
|
||||
renderCopyTextarea('HTTP URL', ownershipHttp.http_url || '', true, 2)
|
||||
+ renderCopyTextarea('HTTP Body', ownershipHttp.http_body || '', true, 3)
|
||||
);
|
||||
}
|
||||
|
||||
var records = $.isArray(row.ssl_validation_records) ? row.ssl_validation_records : [];
|
||||
if(records.length > 0){
|
||||
var recordsHtml = '';
|
||||
for(var i = 0; i < records.length; i++){
|
||||
var item = records[i] || {};
|
||||
var emails = $.isArray(item.emails) ? item.emails.join('\n') : '';
|
||||
recordsHtml += '<div class="panel panel-default" style="margin-bottom:12px;">';
|
||||
recordsHtml += '<div class="panel-heading"><strong>证书校验记录 #' + (i + 1) + '</strong><span class="pull-right">' + formatStatusText(item.status || '-') + '</span></div>';
|
||||
recordsHtml += '<div class="panel-body">';
|
||||
recordsHtml += renderCopyInput('TXT 名称', item.txt_name || '', true);
|
||||
recordsHtml += renderCopyTextarea('TXT 值', item.txt_value || '', true, 3);
|
||||
recordsHtml += renderCopyInput('CNAME 名称', item.cname_name || '', true);
|
||||
recordsHtml += renderCopyTextarea('CNAME 目标', item.cname_target || '', true, 2);
|
||||
recordsHtml += renderCopyTextarea('HTTP URL', item.http_url || '', true, 2);
|
||||
recordsHtml += renderCopyTextarea('HTTP Body', item.http_body || '', true, 3);
|
||||
recordsHtml += renderCopyTextarea('邮箱地址', emails, false, 2);
|
||||
recordsHtml += '</div></div>';
|
||||
}
|
||||
html += renderSection('证书校验记录', recordsHtml);
|
||||
}else{
|
||||
html += '<div class="alert alert-warning">Cloudflare 当前尚未返回证书校验记录,请先等待状态进入 <code>pending_validation</code>,再点击“刷新校验”或稍后刷新列表。</div>';
|
||||
}
|
||||
|
||||
if(row.validation_errors){
|
||||
html += renderSection('错误信息', renderCopyTextarea('错误信息', row.validation_errors, false, 3));
|
||||
}
|
||||
|
||||
$("#verificationContent").html(html);
|
||||
}
|
||||
|
||||
function renderSummaryCard(title, value){
|
||||
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + value + '</div></div>';
|
||||
}
|
||||
|
||||
function renderSection(title, body){
|
||||
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + body + '</div></div>';
|
||||
}
|
||||
|
||||
function renderCopyInput(label, value, copyable){
|
||||
var safeValue = String(value || '');
|
||||
if(!safeValue){
|
||||
return '';
|
||||
}
|
||||
var html = '<div class="form-group">';
|
||||
html += '<label>' + htmlEscape(label) + '</label>';
|
||||
if(copyable){
|
||||
html += '<div class="input-group">';
|
||||
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
||||
html += '<span class="input-group-btn"><button type="button" class="btn btn-default" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></span>';
|
||||
html += '</div>';
|
||||
}else{
|
||||
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderCopyTextarea(label, value, copyable, rows){
|
||||
var safeValue = String(value || '');
|
||||
if(!safeValue){
|
||||
return '';
|
||||
}
|
||||
var html = '<div class="form-group">';
|
||||
html += '<label>' + htmlEscape(label) + '</label>';
|
||||
html += '<textarea class="form-control" rows="' + (rows || 3) + '" readonly>' + htmlEscape(safeValue) + '</textarea>';
|
||||
if(copyable){
|
||||
html += '<div class="text-right" style="margin-top:8px;"><button type="button" class="btn btn-default btn-xs" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function formatStatusText(value){
|
||||
var text = value || '-';
|
||||
if(text === '-'){
|
||||
return '<span class="text-muted">-</span>';
|
||||
}
|
||||
return formatStatus(text);
|
||||
}
|
||||
|
||||
function copyEncodedValue(btn){
|
||||
copyText(decodeURIComponent($(btn).attr('data-copy') || ''));
|
||||
}
|
||||
|
||||
function copyText(text){
|
||||
var value = String(text || '');
|
||||
if(!value){
|
||||
layer.msg('没有可复制的内容');
|
||||
return;
|
||||
}
|
||||
if(navigator.clipboard && window.isSecureContext){
|
||||
navigator.clipboard.writeText(value).then(function(){
|
||||
layer.msg('已复制', {icon: 1, time: 1000});
|
||||
}).catch(function(){
|
||||
fallbackCopyText(value);
|
||||
});
|
||||
return;
|
||||
}
|
||||
fallbackCopyText(value);
|
||||
}
|
||||
|
||||
function fallbackCopyText(text){
|
||||
var $temp = $('<textarea readonly></textarea>');
|
||||
$('body').append($temp);
|
||||
$temp.val(text).select();
|
||||
try{
|
||||
document.execCommand('copy');
|
||||
layer.msg('已复制', {icon: 1, time: 1000});
|
||||
}catch(e){
|
||||
layer.alert('复制失败,请手动复制', {icon: 2});
|
||||
}
|
||||
$temp.remove();
|
||||
}
|
||||
|
||||
function deleteHostname(id, hostname){
|
||||
layer.confirm('确定要删除自定义主机名 ' + hostname + ' 吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/hostnames/delete/1',
|
||||
data: {hostname_id: id, hostname: hostname},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
refreshHostnameList();
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadFallbackOrigin(){
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/fallback/get/1',
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
if(res.code === 0){
|
||||
$("#fallbackOrigin").val((res.data && res.data.origin) ? res.data.origin : '');
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveFallbackOrigin(){
|
||||
var origin = $.trim($("#fallbackOrigin").val());
|
||||
if(!origin){
|
||||
layer.msg('请输入 Fallback Origin');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/fallback/set/1',
|
||||
data: {origin: origin},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
$("#fallbackOrigin").val(res.data.origin || origin);
|
||||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearFallbackOrigin(){
|
||||
layer.confirm('确定要清空 Fallback Origin 吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/fallback/delete/1',
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
$("#fallbackOrigin").val('');
|
||||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function htmlEscape(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
@@ -57,6 +57,64 @@ class Cloudflare extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
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'));
|
||||
if ($origin !== '') {
|
||||
$this->validateCustomOrigin($origin);
|
||||
}
|
||||
|
||||
$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 . ' -> ' . ($origin !== '' ? $origin : '清空源站'));
|
||||
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 {
|
||||
@@ -570,28 +628,135 @@ class Cloudflare extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
private function extractCustomHostnameSslPayload(array $row): array
|
||||
{
|
||||
$ssl = isset($row['ssl']) && is_array($row['ssl']) ? $row['ssl'] : [];
|
||||
$payload = [
|
||||
'method' => trim((string)($ssl['method'] ?? 'http')),
|
||||
'type' => trim((string)($ssl['type'] ?? 'dv')),
|
||||
];
|
||||
if ($payload['method'] === '') {
|
||||
$payload['method'] = 'http';
|
||||
}
|
||||
if ($payload['type'] === '') {
|
||||
$payload['type'] = 'dv';
|
||||
}
|
||||
if (!empty($ssl['bundle_method'])) {
|
||||
$payload['bundle_method'] = trim((string)$ssl['bundle_method']);
|
||||
}
|
||||
if (!empty($ssl['certificate_authority'])) {
|
||||
$payload['certificate_authority'] = trim((string)$ssl['certificate_authority']);
|
||||
}
|
||||
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) {
|
||||
$validationErrors[] = trim((string)($item['message'] ?? $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_status' => trim((string)($ssl['status'] ?? '')),
|
||||
'ssl_method' => trim((string)($ssl['method'] ?? '')),
|
||||
'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_filter($validationErrors)),
|
||||
'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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,15 @@ class CloudflareEnhanceService
|
||||
}
|
||||
}
|
||||
|
||||
public function getCustomHostname(string $zoneId, string $hostnameId): array
|
||||
{
|
||||
try {
|
||||
return $this->requestResult('GET', '/zones/' . $zoneId . '/custom_hostnames/' . trim($hostnameId));
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取自定义主机名详情', $e, 'SSL and Certificates:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function createCustomHostname(string $zoneId, string $hostname, ?string $customOriginServer = null): array
|
||||
{
|
||||
$hostname = $this->normalizeHostname($hostname);
|
||||
@@ -104,6 +113,22 @@ class CloudflareEnhanceService
|
||||
}
|
||||
}
|
||||
|
||||
public function updateCustomHostname(string $zoneId, string $hostnameId, array $payload): array
|
||||
{
|
||||
if (isset($payload['custom_origin_server']) && $payload['custom_origin_server'] !== null) {
|
||||
$payload['custom_origin_server'] = $this->normalizeHostname($payload['custom_origin_server']);
|
||||
}
|
||||
if (isset($payload['hostname']) && $payload['hostname'] !== null) {
|
||||
$payload['hostname'] = $this->normalizeHostname($payload['hostname']);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->requestResult('PATCH', '/zones/' . $zoneId . '/custom_hostnames/' . trim($hostnameId), [], $payload);
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('更新自定义主机名', $e, 'SSL and Certificates:Write');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteCustomHostname(string $zoneId, string $hostnameId): bool
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="alert alert-info">
|
||||
<strong>说明:</strong> 这里管理 Cloudflare 自定义主机名、证书状态与 Fallback Origin。
|
||||
<strong>说明:</strong> 这里管理 Cloudflare 自定义主机名、证书状态、证书校验与 Fallback Origin。
|
||||
</div>
|
||||
|
||||
<div class="well well-sm">
|
||||
@@ -27,11 +27,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
|
||||
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
|
||||
<a href="javascript:searchClear()" class="btn btn-default" title="刷新自定义主机名列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
<a href="javascript:openAddDialog()" class="btn btn-success"><i class="fa fa-plus"></i> 添加自定义主机名</a>
|
||||
</form>
|
||||
<div class="clearfix" style="margin-bottom:15px;">
|
||||
<div class="pull-left">
|
||||
<a href="javascript:refreshHostnameList()" class="btn btn-default" title="刷新自定义主机名列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
<a href="javascript:openAddDialog()" class="btn btn-success"><i class="fa fa-plus"></i> 添加自定义主机名</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="listTable"></table>
|
||||
</div>
|
||||
@@ -44,20 +45,23 @@
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title">添加自定义主机名</h4>
|
||||
<h4 class="modal-title" id="storeTitle">添加自定义主机名</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" id="form-store">
|
||||
<input type="hidden" name="hostname_id" value="">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">主机名</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="hostname" placeholder="例如 app.example.com 或 *.example.com" required>
|
||||
<p class="help-block" id="hostnameHint">创建后主机名不能直接改名,如需改名请删除后重建。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">自定义源站</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="custom_origin_server" placeholder="可留空,例如 origin.example.com">
|
||||
<p class="help-block">留空表示清空当前自定义源站,回退到 Fallback Origin 或默认源站逻辑。</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -69,6 +73,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-verification" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||
<h4 class="modal-title" id="verificationTitle">证书校验</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="verificationContent"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="refreshHostnameValidation()">刷新校验</button>
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
{block name="script"}
|
||||
<script src="/static/js/layer/layer.js"></script>
|
||||
@@ -77,57 +99,100 @@
|
||||
<script src="/static/js/bootstrapValidator.min.js"></script>
|
||||
<script src="/static/js/custom.js"></script>
|
||||
<script>
|
||||
var currentVerificationHostnameId = '';
|
||||
|
||||
$(document).ready(function(){
|
||||
updateToolbar();
|
||||
$("#form-store").bootstrapValidator();
|
||||
loadFallbackOrigin();
|
||||
$("#listTable").bootstrapTable({
|
||||
url: '/cloudflare/hostnames/data/{$domainId}',
|
||||
method: 'post',
|
||||
toolbar: '',
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
responseHandler: function(res){
|
||||
if(res.code !== 0){
|
||||
layer.alert(res.msg || '获取自定义主机名失败', {icon: 2});
|
||||
return {total: 0, rows: []};
|
||||
}
|
||||
return res;
|
||||
},
|
||||
responseHandler: hostnameResponseHandler,
|
||||
columns: [
|
||||
{field: 'hostname', title: '主机名'},
|
||||
{field: 'custom_origin_server', title: '自定义源站', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'ssl_status', title: '证书状态', formatter: formatStatus},
|
||||
{field: 'verification_status', title: '验证状态', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'ssl_validation_status', title: '证书校验', formatter: formatStatus},
|
||||
{field: 'verification_status', title: '所有权校验', formatter: formatStatus},
|
||||
{field: 'created_on', title: '创建时间', formatter: function(v){ return v || '-'; }},
|
||||
{field: 'validation_errors', title: '错误信息', formatter: function(v){ return v || '-'; }},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row){
|
||||
return '<a href="javascript:deleteHostname(\''+row.id+'\', \''+htmlEscape(row.hostname)+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
return ''
|
||||
+ '<a href="javascript:openEditDialog(\''+row.id+'\')" class="btn btn-info btn-xs">编辑</a> '
|
||||
+ '<a href="javascript:openVerificationDialog(\''+row.id+'\')" class="btn btn-primary btn-xs">校验</a> '
|
||||
+ '<a href="javascript:deleteHostname(\''+row.id+'\', \''+htmlEscape(row.hostname)+'\')" class="btn btn-danger btn-xs">删除</a>';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
function hostnameResponseHandler(res){
|
||||
if(res.code !== 0){
|
||||
layer.alert(res.msg || '获取自定义主机名失败', {icon: 2});
|
||||
return {total: 0, rows: []};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function refreshHostnameList(){
|
||||
$("#listTable").bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
function formatStatus(value){
|
||||
var v = (value || '').toLowerCase();
|
||||
if(v === 'active' || v === 'active_deployed'){
|
||||
var v = String(value || '').toLowerCase();
|
||||
if(v === 'active' || v === 'active_deployed' || v === 'valid'){
|
||||
return '<span class="label label-success">'+htmlEscape(value)+'</span>';
|
||||
}
|
||||
if(v === 'pending' || v === 'pending_validation' || v === 'initializing'){
|
||||
if(v === 'pending' || v === 'pending_validation' || v === 'initializing' || v === 'in_progress'){
|
||||
return '<span class="label label-warning">'+htmlEscape(value || '-')+'</span>';
|
||||
}
|
||||
if(v){
|
||||
if(v && v !== '-'){
|
||||
return '<span class="label label-danger">'+htmlEscape(value)+'</span>';
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
function openAddDialog(){
|
||||
function getHostnameRow(id){
|
||||
var row = $("#listTable").bootstrapTable('getRowByUniqueId', id);
|
||||
if(!row){
|
||||
layer.alert('未找到自定义主机名数据,请先刷新列表后重试', {icon: 2});
|
||||
return null;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function resetHostnameForm(){
|
||||
$("#form-store")[0].reset();
|
||||
$("#form-store").data("bootstrapValidator").resetForm();
|
||||
$("#form-store input[name=hostname_id]").val('');
|
||||
$("#form-store input[name=hostname]").prop('readonly', false);
|
||||
$("#form-store").data("bootstrapValidator").resetForm(true);
|
||||
}
|
||||
|
||||
function openAddDialog(){
|
||||
resetHostnameForm();
|
||||
$("#storeTitle").text('添加自定义主机名');
|
||||
$("#hostnameHint").text('创建后主机名不能直接改名,如需改名请删除后重建。');
|
||||
$("#modal-store").modal('show');
|
||||
}
|
||||
|
||||
function openEditDialog(id){
|
||||
var row = getHostnameRow(id);
|
||||
if(!row){
|
||||
return;
|
||||
}
|
||||
resetHostnameForm();
|
||||
$("#storeTitle").text('编辑自定义主机名');
|
||||
$("#hostnameHint").text('主机名不可直接改名,当前仅支持修改或清空自定义源站。');
|
||||
$("#form-store input[name=hostname_id]").val(row.id);
|
||||
$("#form-store input[name=hostname]").val(row.hostname).prop('readonly', true);
|
||||
$("#form-store input[name=custom_origin_server]").val(row.custom_origin_server || '');
|
||||
$("#modal-store").modal('show');
|
||||
}
|
||||
|
||||
@@ -136,18 +201,28 @@ function submitHostname(){
|
||||
if(!$("#form-store").data("bootstrapValidator").isValid()){
|
||||
return;
|
||||
}
|
||||
var hostnameId = $.trim($("#form-store input[name=hostname_id]").val());
|
||||
var url = hostnameId ? '/cloudflare/hostnames/update/{$domainId}' : '/cloudflare/hostnames/add/{$domainId}';
|
||||
var successMsg = hostnameId ? '更新自定义主机名成功' : '创建自定义主机名成功';
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/hostnames/add/{$domainId}',
|
||||
url: url,
|
||||
data: $("#form-store").serialize(),
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
$("#modal-store").modal('hide');
|
||||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||||
searchRefresh();
|
||||
layer.msg(res.msg || successMsg, {icon: 1, time: 1200});
|
||||
if(res.data && res.data.id){
|
||||
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
||||
if(!$("#listTable").bootstrapTable('getRowByUniqueId', res.data.id)){
|
||||
refreshHostnameList();
|
||||
}
|
||||
}else{
|
||||
refreshHostnameList();
|
||||
}
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
@@ -159,8 +234,191 @@ function submitHostname(){
|
||||
});
|
||||
}
|
||||
|
||||
function openVerificationDialog(id){
|
||||
var row = getHostnameRow(id);
|
||||
if(!row){
|
||||
return;
|
||||
}
|
||||
currentVerificationHostnameId = id;
|
||||
renderVerificationDialog(row);
|
||||
$("#modal-verification").modal('show');
|
||||
}
|
||||
|
||||
function refreshHostnameValidation(){
|
||||
if(!currentVerificationHostnameId){
|
||||
layer.msg('请先选择自定义主机名');
|
||||
return;
|
||||
}
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/cloudflare/hostnames/refresh/{$domainId}',
|
||||
data: {hostname_id: currentVerificationHostnameId},
|
||||
dataType: 'json',
|
||||
success: function(res){
|
||||
layer.close(ii);
|
||||
if(res.code === 0){
|
||||
if(res.data && res.data.id){
|
||||
$("#listTable").bootstrapTable('updateByUniqueId', {id: res.data.id, row: res.data});
|
||||
renderVerificationDialog(res.data);
|
||||
}else{
|
||||
refreshHostnameList();
|
||||
}
|
||||
layer.msg(res.msg, {icon: 1, time: 1200});
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.alert('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderVerificationDialog(row){
|
||||
$("#verificationTitle").text('证书校验 - ' + row.hostname);
|
||||
var html = '';
|
||||
html += '<div class="alert alert-info"><strong>说明:</strong> 下列值直接来自 Cloudflare 返回结果,可直接复制到 DNS、源站或验证目录中。点击“刷新校验”会重新向 Cloudflare 发起一次校验。</div>';
|
||||
html += '<div class="row">';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('证书状态', formatStatusText(row.ssl_status))+'</div>';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('证书校验', formatStatusText(row.ssl_validation_status))+'</div>';
|
||||
html += '<div class="col-sm-4">'+renderSummaryCard('所有权校验', formatStatusText(row.verification_status))+'</div>';
|
||||
html += '</div>';
|
||||
|
||||
var ownership = row.ownership_verification || {};
|
||||
if(ownership.name || ownership.value){
|
||||
html += renderSection('所有权 TXT 校验',
|
||||
renderCopyInput('记录类型', ownership.type || 'txt', false)
|
||||
+ renderCopyInput('TXT 名称', ownership.name || '', true)
|
||||
+ renderCopyTextarea('TXT 值', ownership.value || '', true, 3)
|
||||
);
|
||||
}
|
||||
|
||||
var ownershipHttp = row.ownership_verification_http || {};
|
||||
if(ownershipHttp.http_url || ownershipHttp.http_body){
|
||||
html += renderSection('所有权 HTTP 校验',
|
||||
renderCopyTextarea('HTTP URL', ownershipHttp.http_url || '', true, 2)
|
||||
+ renderCopyTextarea('HTTP Body', ownershipHttp.http_body || '', true, 3)
|
||||
);
|
||||
}
|
||||
|
||||
var records = $.isArray(row.ssl_validation_records) ? row.ssl_validation_records : [];
|
||||
if(records.length > 0){
|
||||
var recordsHtml = '';
|
||||
for(var i = 0; i < records.length; i++){
|
||||
var item = records[i] || {};
|
||||
var emails = $.isArray(item.emails) ? item.emails.join('\n') : '';
|
||||
recordsHtml += '<div class="panel panel-default" style="margin-bottom:12px;">';
|
||||
recordsHtml += '<div class="panel-heading"><strong>证书校验记录 #' + (i + 1) + '</strong><span class="pull-right">' + formatStatusText(item.status || '-') + '</span></div>';
|
||||
recordsHtml += '<div class="panel-body">';
|
||||
recordsHtml += renderCopyInput('TXT 名称', item.txt_name || '', true);
|
||||
recordsHtml += renderCopyTextarea('TXT 值', item.txt_value || '', true, 3);
|
||||
recordsHtml += renderCopyInput('CNAME 名称', item.cname_name || '', true);
|
||||
recordsHtml += renderCopyTextarea('CNAME 目标', item.cname_target || '', true, 2);
|
||||
recordsHtml += renderCopyTextarea('HTTP URL', item.http_url || '', true, 2);
|
||||
recordsHtml += renderCopyTextarea('HTTP Body', item.http_body || '', true, 3);
|
||||
recordsHtml += renderCopyTextarea('邮箱地址', emails, false, 2);
|
||||
recordsHtml += '</div></div>';
|
||||
}
|
||||
html += renderSection('证书校验记录', recordsHtml);
|
||||
}else{
|
||||
html += '<div class="alert alert-warning">Cloudflare 当前尚未返回证书校验记录,请先等待状态进入 <code>pending_validation</code>,再点击“刷新校验”或稍后刷新列表。</div>';
|
||||
}
|
||||
|
||||
if(row.validation_errors){
|
||||
html += renderSection('错误信息', renderCopyTextarea('错误信息', row.validation_errors, false, 3));
|
||||
}
|
||||
|
||||
$("#verificationContent").html(html);
|
||||
}
|
||||
|
||||
function renderSummaryCard(title, value){
|
||||
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + value + '</div></div>';
|
||||
}
|
||||
|
||||
function renderSection(title, body){
|
||||
return '<div class="panel panel-default"><div class="panel-heading"><strong>' + htmlEscape(title) + '</strong></div><div class="panel-body">' + body + '</div></div>';
|
||||
}
|
||||
|
||||
function renderCopyInput(label, value, copyable){
|
||||
var safeValue = String(value || '');
|
||||
if(!safeValue){
|
||||
return '';
|
||||
}
|
||||
var html = '<div class="form-group">';
|
||||
html += '<label>' + htmlEscape(label) + '</label>';
|
||||
if(copyable){
|
||||
html += '<div class="input-group">';
|
||||
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
||||
html += '<span class="input-group-btn"><button type="button" class="btn btn-default" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></span>';
|
||||
html += '</div>';
|
||||
}else{
|
||||
html += '<input type="text" class="form-control" readonly value="' + htmlEscape(safeValue) + '">';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderCopyTextarea(label, value, copyable, rows){
|
||||
var safeValue = String(value || '');
|
||||
if(!safeValue){
|
||||
return '';
|
||||
}
|
||||
var html = '<div class="form-group">';
|
||||
html += '<label>' + htmlEscape(label) + '</label>';
|
||||
html += '<textarea class="form-control" rows="' + (rows || 3) + '" readonly>' + htmlEscape(safeValue) + '</textarea>';
|
||||
if(copyable){
|
||||
html += '<div class="text-right" style="margin-top:8px;"><button type="button" class="btn btn-default btn-xs" data-copy="' + encodeURIComponent(safeValue) + '" onclick="copyEncodedValue(this)">复制</button></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function formatStatusText(value){
|
||||
var text = value || '-';
|
||||
if(text === '-'){
|
||||
return '<span class="text-muted">-</span>';
|
||||
}
|
||||
return formatStatus(text);
|
||||
}
|
||||
|
||||
function copyEncodedValue(btn){
|
||||
copyText(decodeURIComponent($(btn).attr('data-copy') || ''));
|
||||
}
|
||||
|
||||
function copyText(text){
|
||||
var value = String(text || '');
|
||||
if(!value){
|
||||
layer.msg('没有可复制的内容');
|
||||
return;
|
||||
}
|
||||
if(navigator.clipboard && window.isSecureContext){
|
||||
navigator.clipboard.writeText(value).then(function(){
|
||||
layer.msg('已复制', {icon: 1, time: 1000});
|
||||
}).catch(function(){
|
||||
fallbackCopyText(value);
|
||||
});
|
||||
return;
|
||||
}
|
||||
fallbackCopyText(value);
|
||||
}
|
||||
|
||||
function fallbackCopyText(text){
|
||||
var $temp = $('<textarea readonly></textarea>');
|
||||
$('body').append($temp);
|
||||
$temp.val(text).select();
|
||||
try{
|
||||
document.execCommand('copy');
|
||||
layer.msg('已复制', {icon: 1, time: 1000});
|
||||
}catch(e){
|
||||
layer.alert('复制失败,请手动复制', {icon: 2});
|
||||
}
|
||||
$temp.remove();
|
||||
}
|
||||
|
||||
function deleteHostname(id, hostname){
|
||||
layer.confirm('确定要删除自定义主机名 '+hostname+' 吗?', {title: '提示', icon: 0}, function(){
|
||||
layer.confirm('确定要删除自定义主机名 ' + hostname + ' 吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
@@ -172,7 +430,7 @@ function deleteHostname(id, hostname){
|
||||
if(res.code === 0){
|
||||
layer.closeAll();
|
||||
layer.msg(res.msg, {icon: 1, time: 1000});
|
||||
searchRefresh();
|
||||
refreshHostnameList();
|
||||
}else{
|
||||
layer.alert(res.msg, {icon: 2});
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ Route::group(function () {
|
||||
Route::get('/cloudflare/hostnames/:id', 'cloudflare/hostnames');
|
||||
Route::post('/cloudflare/hostnames/data/:id', 'cloudflare/hostnames_data');
|
||||
Route::post('/cloudflare/hostnames/add/:id', 'cloudflare/hostnames_add');
|
||||
Route::post('/cloudflare/hostnames/update/:id', 'cloudflare/hostnames_update');
|
||||
Route::post('/cloudflare/hostnames/refresh/:id', 'cloudflare/hostnames_refresh');
|
||||
Route::post('/cloudflare/hostnames/delete/:id', 'cloudflare/hostnames_delete');
|
||||
Route::post('/cloudflare/fallback/get/:id', 'cloudflare/fallback_get');
|
||||
Route::post('/cloudflare/fallback/set/:id', 'cloudflare/fallback_set');
|
||||
|
||||
Reference in New Issue
Block a user