8 Commits
2.8.0 ... 2.8.1

Author SHA1 Message Date
net909
933f98a909 2.8.1 2025-07-06 12:21:04 +08:00
net909
3beb0ec637 修复阿里云WAF部署 2025-07-06 12:19:02 +08:00
net909
7a92ad7094 Merge branch 'main' of ssh://ssh.github.com:443/netcccyun/dnsmgr 2025-07-06 11:17:06 +08:00
net909
5dc4dbfabd 增加Edgeone IP优选,修复EO海外版部署失败 2025-07-06 11:16:44 +08:00
DearTanker
291f715e91 优化记录值复制功能 (#266)
1、修复无法复制包含双引号的 TXT 记录值
2、调整 MX 记录值的复制按钮到右侧,因为复制不需要复制优先级。
2025-07-04 09:43:55 +08:00
net909
4f15d4d7a2 调整快速打开按钮位置 2025-07-03 19:48:34 +08:00
DearTanker
11e83860b7 添加 自定义样式功能、主机记录增加快速打开链接 (#263)
* 添加支持自定义样式功能。

* 在主机记录列中增加一个新窗口打开链接的小按钮,方便快速访问。

* 记录值增加快速复制按钮,并用 padding 来调整了图标的间距避免空格
2025-07-03 19:37:34 +08:00
net909
479af4fe5f 修复批量操作证书和部署任务 2025-06-30 12:44:31 +08:00
16 changed files with 169 additions and 50 deletions

View File

@@ -458,7 +458,7 @@ class Cert extends BaseController
$ids = input('post.ids');
$success = 0;
foreach ($ids as $id) {
if (input('post.action') == 'delete') {
if (input('post.act') == 'delete') {
$dcount = DB::name('cert_deploy')->where('oid', $id)->count();
if ($dcount > 0) continue;
try {
@@ -468,7 +468,7 @@ class Cert extends BaseController
Db::name('cert_order')->where('id', $id)->delete();
Db::name('cert_domain')->where('oid', $id)->delete();
$success++;
} elseif (input('post.action') == 'reset') {
} elseif (input('post.act') == 'reset') {
try {
$service = new CertOrderService($id);
$service->cancel();
@@ -476,8 +476,8 @@ class Cert extends BaseController
$success++;
} catch (Exception $e) {
}
} elseif (input('post.action') == 'open' || input('post.action') == 'close') {
$isauto = input('post.action') == 'open' ? 1 : 0;
} elseif (input('post.act') == 'open' || input('post.act') == 'close') {
$isauto = input('post.act') == 'open' ? 1 : 0;
Db::name('cert_order')->where('id', $id)->update(['isauto' => $isauto]);
$success++;
}
@@ -754,21 +754,21 @@ class Cert extends BaseController
if (!$cert) return json(['code' => -1, 'msg' => '证书订单不存在']);
}
foreach ($ids as $id) {
if (input('post.action') == 'delete') {
if (input('post.act') == 'delete') {
Db::name('cert_deploy')->where('id', $id)->delete();
$success++;
} elseif (input('post.action') == 'reset') {
} elseif (input('post.act') == 'reset') {
try {
$service = new CertDeployService($id);
$service->reset();
$success++;
} catch (Exception $e) {
}
} elseif (input('post.action') == 'open' || input('post.action') == 'close') {
$active = input('post.action') == 'open' ? 1 : 0;
} elseif (input('post.act') == 'open' || input('post.act') == 'close') {
$active = input('post.act') == 'open' ? 1 : 0;
Db::name('cert_deploy')->where('id', $id)->update(['active' => $active]);
$success++;
} elseif (input('post.action') == 'cert') {
} elseif (input('post.act') == 'cert') {
Db::name('cert_deploy')->where('id', $id)->update(['oid' => $certid]);
$success++;
}

View File

@@ -998,6 +998,24 @@ class DeployHelper
'show' => 'product==\'lighthouse\'||product==\'ddos\'',
'required' => true,
],
'site_type' => [
'name' => '站点类型',
'type' => 'select',
'options' => [
['value'=>'cn', 'label'=>'国内站'],
['value'=>'intl', 'label'=>'国际站'],
],
'value' => 'cn',
'show' => 'product==\'teo\'',
'required' => true,
],
'site_id' => [
'name' => '站点ID',
'type' => 'input',
'placeholder' => '类似于zone-xxxx在站点列表或概览页面查看',
'show' => 'product==\'teo\'',
'required' => true,
],
'domain' => [
'name' => '绑定的域名',
'type' => 'input',
@@ -1866,6 +1884,12 @@ class DeployHelper
'show' => 'format==\'pfx\'',
'required' => true,
],
'cmd_pre' => [
'name' => '上传前执行命令',
'type' => 'textarea',
'show' => 'format==\'pem\'||uptype==0',
'placeholder' => '可留空,上传前执行脚本命令',
],
'cmd' => [
'name' => '上传完执行命令',
'type' => 'textarea',

View File

@@ -264,6 +264,12 @@ class aliyun implements DeployInterface
$domain = $config['domain'];
if (empty($domain)) throw new Exception('WAF绑定域名不能为空');
if ($config['region'] == 'ap-southeast-1') {
$cert_id .= '-ap-southeast-1';
} else {
$cert_id .= '-cn-hangzhou';
}
$endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com';
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2021-10-01', $this->proxy);
@@ -292,17 +298,19 @@ class aliyun implements DeployInterface
} catch (Exception $e) {
throw new Exception('查询CNAME接入详情失败' . $e->getMessage());
}
if (!isset($data['Listen'])) {
throw new Exception('没有找到' . $domain . '监听器');
}
if (isset($data['Listen']['CertId'])) {
$old_cert_id = $data['Listen']['CertId'];
if (strpos($old_cert_id, '-')) $old_cert_id = substr($old_cert_id, 0, strpos($old_cert_id, '-'));
if (!empty($old_cert_id) && $old_cert_id == $cert_id) {
$this->log('WAF域名 ' . $domain . ' 证书已配置,无需重复操作');
return;
}
}
$data['Listen']['CertId'] = $cert_id . '-cn-hangzhou';
$data['Listen']['CertId'] = $cert_id;
if (empty($data['Listen']['HttpsPorts'])) $data['Listen']['HttpsPorts'] = [443];
$data['Redirect']['Backends'] = $data['Redirect']['AllBackends'];
$param = [

View File

@@ -23,6 +23,14 @@ class ssh implements DeployInterface
public function deploy($fullchain, $privatekey, $config, &$info)
{
$connection = $this->connect();
if (isset($config['cmd_pre']) && !empty($config['cmd_pre'])) {
$cmds = explode("\n", $config['cmd_pre']);
foreach ($cmds as $cmd) {
$cmd = trim($cmd);
if (empty($cmd)) continue;
$this->exec($connection, $cmd);
}
}
$sftp = ssh2_sftp($connection);
if ($config['format'] == 'pem') {
$stream = fopen("ssh2.sftp://$sftp{$config['pem_cert_file']}", 'w');

View File

@@ -60,6 +60,8 @@ class tencent implements DeployInterface
return $this->deploy_clb($cert_id, $config);
} elseif ($config['product'] == 'scf') {
return $this->deploy_scf($cert_id, $config);
} elseif ($config['product'] == 'teo' && isset($config['site_id'])) {
return $this->deploy_teo($cert_id, $config);
} else {
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
if ($config['product'] == 'waf') {
@@ -122,11 +124,17 @@ class tencent implements DeployInterface
} else {
$instance_ids = [$instance_id];
}
if ($product == 'cdn') {
$instance_ids = array_map(function ($id) {
return $id . '|on';
}, $instance_ids);
}
$param = [
'CertificateId' => $cert_id,
'InstanceIdList' => $instance_ids,
'ResourceType' => $product,
];
if ($product == 'live') $param['Status'] = 1;
$data = $this->client->request('DeployCertificateInstance', $param);
$this->log(json_encode($data));
$this->log(strtoupper($product) . '实例 ' . $instance_id . ' 部署证书成功!');
@@ -253,6 +261,26 @@ class tencent implements DeployInterface
$this->log('云函数自定义域名 ' . $config['domain'] . ' 部署证书成功!');
}
private function deploy_teo($cert_id, $config)
{
if (empty($config['site_id'])) throw new Exception('站点ID不能为空');
if (empty($config['domain'])) throw new Exception('绑定的域名不能为空');
$endpoint = isset($config['site_type']) && $config['site_type'] == 'intl' ? 'teo.intl.tencentcloudapi.com' : 'teo.tencentcloudapi.com';
$client = new TencentCloud($this->SecretId, $this->SecretKey, $endpoint, 'teo', '2022-09-01', null, $this->proxy);
$hosts = explode(',', $config['domain']);
$param = [
'ZoneId' => $config['site_id'],
'Hosts' => $hosts,
'Mode' => 'sslcert',
'ServerCertInfo' => [[
'CertId' => $cert_id
]]
];
$data = $client->request('ModifyHostsCertificate', $param);
$this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!');
}
public function setLogger($func)
{
$this->logger = $func;

View File

@@ -39,9 +39,7 @@ class OptimizeService
public function get_ip_address($cdn_type = 1, $ip_type = 'v4')
{
$api = config_get('optimize_ip_api', 0);
if ($api == 2) {
$url = 'https://api.345673.xyz/get_data';
} elseif ($api == 1) {
if ($api == 1) {
$url = 'https://api.hostmonit.com/get_optimization_ip';
} else {
$url = 'https://www.wetest.vip/api/cf2dns/';
@@ -51,6 +49,8 @@ class OptimizeService
$url .= 'get_cloudfront_ip';
} elseif ($cdn_type == 3) {
$url .= 'get_gcore_ip';
} elseif ($cdn_type == 4) {
$url .= 'get_edgeone_ip';
}
}
$params = [

View File

@@ -454,7 +454,7 @@ function operation(action){
$.ajax({
type : 'POST',
url : '/cert/order/operation',
data : {action: action, ids: ids},
data : {act: action, ids: ids},
dataType : 'json',
success : function(data) {
layer.close(ii);

View File

@@ -317,7 +317,7 @@ function operation(action){
$.ajax({
type : 'POST',
url : '/cert/deploy/operation',
data : {action: action, ids: ids},
data : {act: action, ids: ids},
dataType : 'json',
success : function(data) {
layer.close(ii);
@@ -337,7 +337,7 @@ function batch_set_cert(ids){
$.ajax({
type : 'POST',
url : '/cert/deploy/operation',
data : {action: 'cert', ids: ids, certid: text},
data : {act: 'cert', ids: ids, certid: text},
dataType : 'json',
success : function(data) {
layer.close(ii);

View File

@@ -12,6 +12,9 @@
<link href="/static/css/app.min.css" rel="stylesheet">
<link href="/static/css/skins/{$skin}.css" rel="stylesheet">
<link href="/static/css/bootstrap-table.css?v=2" rel="stylesheet"/>
{if file_exists(public_path() . 'static/css/custom.css')}
<link href="/static/css/custom.css" rel="stylesheet">
{/if}
<script src="{$cdnpublic}jquery/3.6.4/jquery.min.js"></script>
<!--[if lt IE 9]>
<script src="{$cdnpublic}html5shiv/3.7.3/html5shiv.min.js"></script>

View File

@@ -175,7 +175,7 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
<div id="searchbox1">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="keyword" placeholder="输入关键字">
<input type="text" class="form-control" name="keyword" placeholder="输入关键字">
</div>
<div class="form-group">
<select name="status" class="form-control"><option value="">所有状态</option><option value="1">启用</option><option value="0">暂停</option></select>
@@ -213,13 +213,13 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
</select>
</div>
<div class="form-group">
<input type="text" class="form-control" name="subdomain" placeholder="输入主机记录">
<input type="text" class="form-control" name="subdomain" placeholder="输入主机记录">
</div>
<div class="form-group">
<select name="line" class="form-control"><option value="">全部线路</option></select>
</div>
<div class="form-group">
<input type="text" class="form-control" name="value" placeholder="输入记录值">
<input type="text" class="form-control" name="value" placeholder="输入记录值">
</div>
<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>
@@ -227,9 +227,9 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
</div>
</form>
<table id="listTable">
<table id="listTable">
</table>
</div>
</div>
</div>
</div>
</div>
@@ -269,10 +269,10 @@ $(document).ready(function(){
visible: false,
title: '记录ID'
},
{
field: 'Name',
title: '主机记录'
},
{
field: 'Name',
title: '主机记录'
},
{
field: 'Type',
title: '记录类型',
@@ -282,18 +282,26 @@ $(document).ready(function(){
return value;
}
},
{
{
field: 'LineName',
title: '线路类型'
},
{
field: 'Value',
title: '记录值',
formatter: function(value, row, index) {
if(row.Type == 'MX') return value + ' | '+row.MX;
return value;
}
},
field: 'Value',
title: '记录值',
formatter: function(value, row, index) {
var copyId = 'copy-value-' + row.RecordId;
if(row.Type == 'MX') {
// 只复制 mx.yandex.net按钮在其右侧优先级单独显示
return '<span id="'+copyId+'" data-value="'+htmlEscape(value)+'">'+value+'</span>'
+ '<a href="javascript:void(0);" title="复制记录值" onclick="copyToClipboard(null, \'#'+copyId+'\')" style="padding-left:6px;"><i class=\"fa fa-copy\"></i></a>'
+ '<span class="mx-priority"> | '+row.MX+'</span>';
} else {
return '<span id="'+copyId+'" data-value="'+htmlEscape(value)+'">'+value+'</span>'
+ '<a href="javascript:void(0);" title="复制记录值" onclick="copyToClipboard(null, \'#'+copyId+'\')" style="padding-left:6px;"><i class=\"fa fa-copy\"></i></a>';
}
}
},
{
field: 'TTL',
title: 'TTL'
@@ -339,8 +347,12 @@ $(document).ready(function(){
}
html += '<a href="javascript:delItem(\''+row.RecordId+'\')" class="btn btn-danger btn-xs">删除</a>&nbsp;&nbsp;';
if(dnsconfig.remark == 1){
html += '<a href="javascript:setRemark(\''+row.RecordId+'\')" class="btn btn-info btn-xs">备注</a>';
html += '<a href="javascript:setRemark(\''+row.RecordId+'\')" class="btn btn-info btn-xs">备注</a>&nbsp;&nbsp;';
}
if(row.Name === "@") var domain = "{$domainName}";
else var domain = row.Name + ".{$domainName}";
domain = domain.replace(/\*/g, 'www');
html += '<a href="http://' + domain + '" target="_blank" title="访问域名" class="btn btn-default btn-xs"><i class="fa fa-external-link"></i></a>';
return html;
}
},
@@ -719,5 +731,39 @@ function advanceSearch(){
$("#searchbox1").slideDown();
}
}
function copyToClipboard(text, selector) {
if (!text && selector) {
var el = document.querySelector(selector);
if (el) {
text = el.getAttribute('data-value');
}
}
var tempInput = document.createElement('input');
tempInput.style.position = 'absolute';
tempInput.style.left = '-9999px';
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
if(selector){
var icon = document.querySelector(selector + ' + a i');
if(icon){
var oldClass = icon.className;
icon.className = 'fa fa-check';
setTimeout(function(){ icon.className = oldClass; }, 1000);
}
}
layer.msg('已复制到剪贴板', {icon: 1, time: 600});
}
// 工具函数HTML转义防止XSS
function htmlEscape(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
</script>
{/block}

View File

@@ -129,7 +129,7 @@ new Vue({
cdntypeList: {
1:'CloudFlare',
2:"CloudFront",
3:'Gcore'
4:'EdgeOne'
},
},
watch: {

View File

@@ -75,6 +75,8 @@ $(document).ready(function(){
return 'CloudFront';
}else if(value == 3){
return 'Gcore';
}else if(value == 4){
return 'EdgeOne';
}else{
return '未知';
}

View File

@@ -14,7 +14,7 @@
<div class="panel-heading"><h3 class="panel-title">使用说明</h3></div>
<div class="panel-body">
<p><li>不支持对CloudFlare里的域名添加优选必须使用其他DNS服务商。需开通Cloudflare for SaaS且域名使用CNAME的方式解析到CloudFlare。</li></p>
<p><li>数据接口:<a href="https://www.wetest.vip/" target="_blank" rel="noreferrer">wetest.vip</a> 数据接口支持CloudFlare、CloudFront、Gcore<a href="https://stock.hostmonit.com/" target="_blank" rel="noreferrer">HostMonit</a> 只支持CloudFlare。</li></p>
<p><li>数据接口:<a href="https://www.wetest.vip/" target="_blank" rel="noreferrer">wetest.vip</a> 数据接口支持CloudFlare、CloudFront、EdgeOne<a href="https://stock.hostmonit.com/" target="_blank" rel="noreferrer">HostMonit</a> 只支持CloudFlare。</li></p>
<p><li>接口密钥默认o1zrmHAF为免费KEY可永久免费使用。</li></p>
<p><li>计划任务将以下命令添加到计划任务周期设置为15分钟以上</li></p>
<p><code>cd {:app()->getRootPath()} && php think opiptask</code></p>

24
composer.lock generated
View File

@@ -1507,16 +1507,16 @@
},
{
"name": "topthink/think-orm",
"version": "v4.0.45",
"version": "v4.0.46",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-orm.git",
"reference": "ea2e627af7306f7714e4baafc70943cad954cc77"
"reference": "4bb0a5679a97db8de1c0eb02bbbe179cb3afd901"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/think-orm/zipball/ea2e627af7306f7714e4baafc70943cad954cc77",
"reference": "ea2e627af7306f7714e4baafc70943cad954cc77",
"url": "https://api.github.com/repos/top-think/think-orm/zipball/4bb0a5679a97db8de1c0eb02bbbe179cb3afd901",
"reference": "4bb0a5679a97db8de1c0eb02bbbe179cb3afd901",
"shasum": ""
},
"require": {
@@ -1561,9 +1561,9 @@
],
"support": {
"issues": "https://github.com/top-think/think-orm/issues",
"source": "https://github.com/top-think/think-orm/tree/v4.0.45"
"source": "https://github.com/top-think/think-orm/tree/v4.0.46"
},
"time": "2025-06-24T00:00:32+00:00"
"time": "2025-06-26T06:05:35+00:00"
},
{
"name": "topthink/think-template",
@@ -1727,16 +1727,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e"
"reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e",
"reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42",
"reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42",
"shasum": ""
},
"require": {
@@ -1791,7 +1791,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.3.0"
"source": "https://github.com/symfony/var-dumper/tree/v7.3.1"
},
"funding": [
{
@@ -1807,7 +1807,7 @@
"type": "tidelift"
}
],
"time": "2025-04-27T18:39:23+00:00"
"time": "2025-06-27T19:55:54+00:00"
},
{
"name": "topthink/think-trace",

View File

@@ -31,7 +31,7 @@ return [
'show_error_msg' => true,
'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl',
'version' => '1037',
'version' => '1038',
'dbversion' => '1033'
];

View File