Compare commits

...

10 Commits
2.14.0 ... main

Author SHA1 Message Date
net909
e25d5d76e9 支持部署到阿里云全球加速 2026-02-17 14:30:57 +08:00
net909
c0e72908ab 阿里云ESA超过免费配额之后,自动删除最旧的证书 2026-02-17 13:45:09 +08:00
TomyJan
867785b774
fix: cf 优选增加 xingpingcn.top 接口 & perf: cf 优选 ip 数量上限 50 个 (#396)
* fix: cf 优选增加 xingpingcn.top 接口

* perf: cf 优选 ip 数量上限 50 个

* Update app/service/OptimizeService.php

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update app/service/OptimizeService.php

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update app/controller/Optimizeip.php

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: min num limit

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-12 22:23:21 +08:00
dependabot[bot]
d579ca07af
Bump cccyun/php-whois from 1.2 to 1.3 (#398)
Bumps [cccyun/php-whois](https://github.com/netcccyun/php-whois) from 1.2 to 1.3.
- [Release notes](https://github.com/netcccyun/php-whois/releases)
- [Commits](https://github.com/netcccyun/php-whois/compare/1.2...1.3)

---
updated-dependencies:
- dependency-name: cccyun/php-whois
  dependency-version: '1.3'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 22:20:54 +08:00
net909
7161caf0a5 ssh私钥自动删除 2026-02-02 19:27:31 +08:00
net909
c91b116466 Merge branch 'main' of ssh://ssh.github.com:443/netcccyun/dnsmgr 2026-01-30 23:16:28 +08:00
耗子
b2d27b18a3
feat: 支持AcePanel 3.0部署 (#394) 2026-01-30 23:13:11 +08:00
net909
ee45ddd7ec 新增amh面板部署 2026-01-28 20:53:54 +08:00
TomyJan
9b66b020c9
fix: cf暂停@解析 (#390) 2026-01-27 11:28:56 +08:00
net909
ec16c3fc8b 修复LiteSSL添加DNS失败 2026-01-25 11:12:41 +08:00
22 changed files with 708 additions and 59 deletions

View File

@ -198,7 +198,7 @@ class Domain extends BaseController
if (!empty($id)) {
$select->where('A.id', $id);
} elseif (!empty($kw)) {
$select->whereLike('name|A.remark', '%' . $kw . '%');
$select->whereLike('A.name|A.remark', '%' . $kw . '%');
}
if (!empty($aid)) {
$select->where('A.aid', $aid);

View File

@ -85,8 +85,11 @@ class Optimizeip extends BaseController
if (empty($task['did']) || empty($task['rr']) || empty($task['ip_type']) || empty($task['recordnum']) || empty($task['ttl'])) {
return json(['code' => -1, 'msg' => '必填项不能为空']);
}
if ($task['recordnum'] > 5) {
return json(['code' => -1, 'msg' => '解析数量不能超过5个']);
if ($task['recordnum'] < 1) {
return json(['code' => -1, 'msg' => '解析数量不能少于1个']);
}
if ($task['recordnum'] > 50) {
return json(['code' => -1, 'msg' => '解析数量不能超过50个']);
}
if (Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->find()) {
return json(['code' => -1, 'msg' => '当前域名的优选IP任务已存在']);
@ -109,8 +112,11 @@ class Optimizeip extends BaseController
if (empty($task['did']) || empty($task['rr']) || empty($task['ip_type']) || empty($task['recordnum']) || empty($task['ttl'])) {
return json(['code' => -1, 'msg' => '必填项不能为空']);
}
if ($task['recordnum'] > 5) {
return json(['code' => -1, 'msg' => '解析数量不能超过5个']);
if ($task['recordnum'] < 1) {
return json(['code' => -1, 'msg' => '解析数量不能少于1个']);
}
if ($task['recordnum'] > 50) {
return json(['code' => -1, 'msg' => '解析数量不能超过50个']);
}
if (Db::name('optimizeip')->where('did', $task['did'])->where('rr', $task['rr'])->where('id', '<>', $id)->find()) {
return json(['code' => -1, 'msg' => '当前域名的优选IP任务已存在']);

View File

@ -95,11 +95,12 @@ class System extends BaseController
public function proxytest()
{
if (!checkPermission(2)) return $this->alert('error', '无权限');
$proxy_server = trim($_POST['proxy_server']);
$proxy_port = $_POST['proxy_port'];
$proxy_user = trim($_POST['proxy_user']);
$proxy_pwd = trim($_POST['proxy_pwd']);
$proxy_type = $_POST['proxy_type'];
$proxy_server = input('post.proxy_server', '', 'trim');
$proxy_port = input('post.proxy_port/d', 0);
$proxy_user = input('post.proxy_user', '', 'trim');
$proxy_pwd = input('post.proxy_pwd', '', 'trim');
$proxy_type = input('post.proxy_type', 'http', 'trim');
try {
check_proxy('https://dl.amh.sh/ip.htm', $proxy_server, $proxy_port, $proxy_type, $proxy_user, $proxy_pwd);
} catch (Exception $e) {

View File

@ -257,7 +257,7 @@ location / {
'wildcard' => false,
'max_domains' => 1,
'cname' => false,
'note' => '每个自然年有20张免费证书额度证书到期或吊销不释放额度。需要先进入阿里云控制台-<a href="https://yundun.console.aliyun.com/?p=cas#/certExtend/free/cn-hangzhou" target="_blank" rel="noreferrer">数字证书管理服务</a>,购买个人测试证书资源包。',
'note' => '每个自然年有20张免费证书额度证书到期或吊销不释放额度。需要先进入阿里云控制台-<a href="https://yundun.console.aliyun.com/?p=cas#/instance/TEST/cn-hangzhou" target="_blank" rel="noreferrer">数字证书管理服务</a>,购买个人测试证书资源包。',
'inputs' => [
'AccessKeyId' => [
'name' => 'AccessKeyId',

View File

@ -677,6 +677,65 @@ class DeployHelper
],
],
],
'acepanel' => [
'name' => 'AcePanel',
'class' => 1,
'icon' => 'acepanel.svg',
'desc' => '支持 AcePanel 3.0+ 版本使用',
'note' => '支持 AcePanel 3.0+ 版本使用',
'inputs' => [
'url' => [
'name' => '面板地址',
'type' => 'input',
'placeholder' => 'AcePanel 地址',
'note' => '填写规则如https://192.168.1.100:8888/xxxxxx ,带访问入口但不要带其他后缀',
'required' => true,
],
'id' => [
'name' => '访问令牌ID',
'type' => 'input',
'placeholder' => '1',
'note' => 'AcePanel 设置->用户->访问令牌',
'required' => true,
],
'token' => [
'name' => '访问令牌',
'type' => 'input',
'note' => 'AcePanel 设置->用户->访问令牌',
'placeholder' => '32位字符串',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'taskinputs' => [
'type' => [
'name' => '部署类型',
'type' => 'radio',
'options' => [
'0' => 'AcePanel 网站的证书',
'1' => 'AcePanel 本身的证书',
],
'value' => '0',
'required' => true,
],
'sites' => [
'name' => '网站名称列表',
'type' => 'textarea',
'placeholder' => '填写要部署证书的网站名称,每行一个',
'note' => '填写创建网站时设置的网站唯一名称',
'show' => 'type==0',
'required' => true,
],
],
],
'ratpanel' => [
'name' => '耗子面板',
'class' => 1,
@ -777,6 +836,53 @@ class DeployHelper
],
],
],
'amh' => [
'name' => 'AMH面板',
'class' => 1,
'icon' => 'amh.ico',
'desc' => '',
'note' => null,
'tasknote' => '',
'inputs' => [
'url' => [
'name' => '面板地址',
'type' => 'input',
'placeholder' => 'AMH面板地址',
'note' => '填写规则如http://192.168.1.100:8888 ,不要带其他后缀',
'required' => true,
],
'apikey' => [
'name' => 'API接口密钥',
'type' => 'input',
'placeholder' => '安装amapi软件后查看是密钥不是私钥',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'taskinputs' => [
'env_name' => [
'name' => '环境名称',
'type' => 'input',
'placeholder' => '如lnmp01',
'required' => true,
],
'vhost_name' => [
'name' => '网站名称列表',
'type' => 'textarea',
'placeholder' => '填写要部署证书的网站标识域名,每行一个',
'note' => '网站标识域名一列的值,并非绑定域名',
'required' => true,
],
],
],
'synology' => [
'name' => '群晖面板',
'class' => 1,
@ -1058,6 +1164,7 @@ ctrl+x 保存退出<br/>',
['value'=>'vod', 'label'=>'视频点播'],
['value'=>'fc', 'label'=>'函数计算3.0'],
['value'=>'fc2', 'label'=>'函数计算2.0'],
['value'=>'ga', 'label'=>'全球加速'],
['value'=>'upload', 'label'=>'上传到证书管理'],
],
'value' => 'cdn',
@ -1148,6 +1255,21 @@ ctrl+x 保存退出<br/>',
'note' => '进入NLB实例详情->监听列表复制监听ID只支持TCPSSL监听协议',
'required' => true,
],
'ga_id' => [
'name' => '全球加速实例ID',
'type' => 'input',
'placeholder' => '',
'show' => 'product==\'ga\'',
'required' => true,
],
'ga_listener_id' => [
'name' => '监听ID',
'type' => 'input',
'placeholder' => '',
'show' => 'product==\'ga\'',
'note' => '进入实例详情->监听列表复制监听ID只支持HTTPS监听协议',
'required' => true,
],
'deploy_type' => [
'name' => '部署证书类型',
'type' => 'select',
@ -1156,21 +1278,21 @@ ctrl+x 保存退出<br/>',
['value'=>'1', 'label'=>'扩展证书'],
],
'value' => '0',
'show' => 'product==\'clb\'||product==\'alb\'||product==\'nlb\'',
'show' => 'product==\'clb\'||product==\'alb\'||product==\'nlb\'||product==\'ga\'',
'required' => true,
],
'clb_domain' => [
'name' => '扩展域名',
'type' => 'input',
'placeholder' => '多个域名可使用,分隔',
'show' => 'product==\'clb\'&&deploy_type==1',
'show' => 'product==\'clb\'&&deploy_type==1||product==\'ga\'&&deploy_type==1',
'required' => true,
],
'domain' => [
'name' => '绑定的域名',
'type' => 'input',
'placeholder' => '',
'show' => 'product!=\'esa\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'&&product!=\'upload\'',
'show' => 'product!=\'esa\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'&&product!=\'ga\'&&product!=\'upload\'',
'required' => true,
],
],

View File

@ -374,6 +374,12 @@ class DnsHelper
'placeholder' => '',
'required' => true,
],
'apikey' => [
'name' => 'API密钥/令牌',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'auth' => [
'name' => '认证方式',
'type' => 'radio',
@ -383,12 +389,6 @@ class DnsHelper
],
'value' => '0'
],
'apikey' => [
'name' => 'API密钥/令牌',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',

View File

@ -62,10 +62,14 @@ class customacme implements CertInterface
$dnsList = [];
if (!empty($order['challenges'])) {
$keys = [];
foreach ($order['challenges'] as $opts) {
$key = $opts['key'] . '|' .$opts['value'];
if (in_array($key, $keys)) continue;
$mainDomain = getMainDomain($opts['domain']);
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
$keys[] = $key;
}
}

View File

@ -59,13 +59,17 @@ class litessl implements CertInterface
$dnsList = [];
if (!empty($order['challenges'])) {
$keys = [];
foreach ($order['challenges'] as $opts) {
$key = $opts['key'] . '|' .$opts['value'];
if (in_array($key, $keys)) continue;
$mainDomain = getMainDomain($opts['domain']);
$name = substr($opts['key'], 0, -(strlen($mainDomain) + 1));
/*if (!array_key_exists($mainDomain, $dnsList)) {
$dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "litessl.cn"'];
}*/
$dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']];
$keys[] = $key;
}
}

163
app/lib/deploy/acepanel.php Normal file
View File

@ -0,0 +1,163 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class acepanel implements DeployInterface
{
private $logger;
private $url;
private $id;
private $token;
private $proxy;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->id = $config['id'];
$this->token = $config['token'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->id) || empty($this->token)) throw new Exception('请填写完整面板地址和访问令牌');
$response = $this->request('/user/info', null, 'GET');
$result = json_decode($response, true);
if (isset($result['msg']) && $result['msg'] == "success") {
return true;
} else {
throw new Exception($result['msg'] ?? '面板地址无法连接');
}
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
if ($config['type'] == '1') {
$this->deployPanel($fullchain, $privatekey);
$this->log("面板证书部署成功");
return;
}
$sites = explode("\n", $config['sites']);
$success = 0;
$errmsg = null;
foreach ($sites as $site) {
$site = trim($site);
if (empty($site)) continue;
try {
$this->deploySite($site, $fullchain, $privatekey);
$this->log("网站 {$site} 证书部署成功");
$success++;
} catch (Exception $e) {
$errmsg = $e->getMessage();
$this->log("网站 {$site} 证书部署失败:" . $errmsg);
}
}
if ($success == 0) {
throw new Exception($errmsg ?: '要部署的网站不存在');
}
}
private function deployPanel($fullchain, $privatekey)
{
$data = [
'cert' => $fullchain,
'key' => $privatekey,
];
$response = $this->request('/setting/cert', $data);
$result = json_decode($response, true);
if (isset($result['msg']) && $result['msg'] == "success") {
return true;
} elseif (isset($result['msg'])) {
throw new Exception($result['msg']);
} else {
throw new Exception($response ?: '返回数据解析失败');
}
}
private function deploySite($name, $fullchain, $privatekey)
{
$data = [
'name' => $name,
'cert' => $fullchain,
'key' => $privatekey,
];
$response = $this->request('/website/cert', $data);
$result = json_decode($response, true);
if (isset($result['msg']) && $result['msg'] == "success") {
return true;
} elseif (isset($result['msg'])) {
throw new Exception($result['msg']);
} else {
throw new Exception($response ?: '返回数据解析失败');
}
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
private function request($path, $params, $method = 'POST')
{
$url = $this->url . '/api' . $path;
$body = $method == 'GET' ? null : json_encode($params);
$sign = $this->signRequest($method, $url, $body, $this->id, $this->token);
$response = http_request($url, $body, null, null, [
'Content-Type' => 'application/json',
'X-Timestamp' => $sign['timestamp'],
'Authorization' => 'HMAC-SHA256 Credential=' . $sign['id'] . ', Signature=' . $sign['signature']
], $this->proxy, $method);
return $response['body'];
}
private function signRequest($method, $url, $body, $id, $token)
{
// 解析URL并获取路径
$parsedUrl = parse_url($url);
$path = $parsedUrl['path'];
$query = $parsedUrl['query'] ?? '';
// 规范化路径
$canonicalPath = $path;
if (!str_starts_with($path, '/api')) {
$apiPos = strpos($path, '/api');
if ($apiPos !== false) {
$canonicalPath = substr($path, $apiPos);
}
}
// 构造规范化请求
$canonicalRequest = implode("\n", [
$method,
$canonicalPath,
$query,
hash('sha256', $body ?: '')
]);
// 计算签名
$timestamp = time();
$stringToSign = implode("\n", [
'HMAC-SHA256',
$timestamp,
hash('sha256', $canonicalRequest)
]);
$signature = hash_hmac('sha256', $stringToSign, $token);
return [
'timestamp' => $timestamp,
'signature' => $signature,
'id' => $id
];
}
}

View File

@ -66,6 +66,8 @@ class aliyun implements DeployInterface
$this->deploy_alb($cert_id, $config);
} elseif ($config['product'] == 'nlb') {
$this->deploy_nlb($cert_id, $config);
} elseif ($config['product'] == 'ga') {
$this->deploy_ga($cert_id, $config);
} elseif ($config['product'] == 'upload') {
} else {
throw new Exception('未知的产品类型');
@ -201,11 +203,11 @@ class aliyun implements DeployInterface
}
$this->log('ESA站点 ' . $sitename . ' 查询到' . $data['TotalCount'] . '个SSL证书');
$exist_cert_id = null;
$exist_cert_name = null;
$exist_cert_casid = null;
$exist_cert = null;
$oldest_cert = null;
if ($data['TotalCount'] > 0) {
foreach ($data['Result'] as $cert) {
if ($cert['Type'] == 'free') continue;
$domains = explode(',', $cert['SAN']);
$flag = true;
foreach ($domains as $domain) {
@ -215,11 +217,40 @@ class aliyun implements DeployInterface
}
}
if ($flag) {
$exist_cert_id = $cert['Id'];
$exist_cert_name = $cert['Name'];
$exist_cert_casid = isset($cert['CasId']) ? $cert['CasId'] : null;
$exist_cert = $cert;
break;
}
if (!$oldest_cert) {
$oldest_cert = $cert;
} elseif (strtotime($cert['CreateTime']) < strtotime($oldest_cert['CreateTime'])) {
$oldest_cert = $cert;
}
}
}
if (!$exist_cert) { //新增证书时,若配额已满,则删除最旧的证书
$param = [
'Action' => 'ListInstanceQuotasWithUsage',
'SiteId' => $site_id,
'QuotaNames' => 'customHttpCert',
];
try {
$data = $client->request($param, 'GET');
} catch (Exception $e) {
throw new Exception('查询ESA站点证书配额失败' . $e->getMessage());
}
if (!empty($data['Quotas']) && intval($data['Quotas'][0]['Usage']) >= intval($data['Quotas'][0]['QuotaValue']) && $oldest_cert) {
$param = [
'Action' => 'DeleteCertificate',
'SiteId' => $site_id,
'Id' => $oldest_cert['Id'],
];
try {
$client->request($param, 'GET');
$this->log('ESA站点 ' . $sitename . ' 删除证书 ' . $oldest_cert['Name'] . ' 成功');
} catch (Exception $e) {
throw new Exception('ESA站点 ' . $sitename . ' 删除证书' . $oldest_cert['Name'] . '失败:' . $e->getMessage());
}
}
}
@ -232,10 +263,10 @@ class aliyun implements DeployInterface
'Region' => $config['region'],
];
if ($exist_cert_id) {
$param['Id'] = $exist_cert_id;
if ($exist_cert) {
$param['Id'] = $exist_cert['Id'];
if ($exist_cert_casid == $cas_id) {
if (isset($exist_cert['CasId']) && $exist_cert['CasId'] == $cas_id) {
$this->log('ESA站点 ' . $sitename . ' 证书已配置,无需重复操作');
return;
}
@ -243,8 +274,8 @@ class aliyun implements DeployInterface
$client->request($param);
if ($exist_cert_name) {
$this->log('ESA站点 ' . $sitename . ' 证书 ' . $exist_cert_name . ' 更新成功');
if ($exist_cert) {
$this->log('ESA站点 ' . $sitename . ' 证书 ' . $exist_cert['Name'] . ' 更新成功');
} else {
$this->log('ESA站点 ' . $sitename . ' 证书添加成功!');
}
@ -735,6 +766,84 @@ class aliyun implements DeployInterface
}
}
private function deploy_ga($cert_id, $config)
{
if (empty($config['ga_id'])) throw new Exception('全球加速实例ID不能为空');
if (empty($config['ga_listener_id'])) throw new Exception('全球加速监听ID不能为空');
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'ga.cn-hangzhou.aliyuncs.com', '2019-11-20', $this->proxy);
$cert_id = $cert_id . '-cn-hangzhou';
$deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0;
if ($deploy_type == 1) {
if (empty($config['clb_domain'])) throw new Exception('扩展域名不能为空');
$param = [
'Action' => 'ListListenerCertificates',
'RegionId' => 'cn-hangzhou',
'AcceleratorId' => $config['ga_id'],
'ListenerId' => $config['ga_listener_id'],
];
try {
$data = $client->request($param);
} catch (Exception $e) {
throw new Exception('扩展域名列表查询失败:' . $e->getMessage());
}
$need_add = [];
foreach (explode(',', $config['clb_domain']) as $domain) {
$domainExists = false;
$exist_cert_id = null;
foreach ($data['Certificates'] as $cert) {
if (isset($cert['Domain']) && $domain == $cert['Domain']) {
$domainExists = true;
$exist_cert_id = $cert['CertificateId'];
}
}
if ($domainExists) {
if ($exist_cert_id == $cert_id) {
$this->log('全球加速实例监听扩展域名 ' . $domain . ' 证书已配置');
continue;
}
$param = [
'Action' => 'UpdateAdditionalCertificateWithListener',
'RegionId' => 'cn-hangzhou',
'AcceleratorId' => $config['ga_id'],
'ListenerId' => $config['ga_listener_id'],
'Domain' => $domain,
'CertificateId' => $cert_id,
];
$client->request($param);
$this->log('全球加速实例监听扩展域名 ' . $domain . ' 替换证书成功!');
} else {
$need_add[] = $domain;
}
}
if (count($need_add) > 0) {
$param = [
'Action' => 'AssociateAdditionalCertificatesWithListener',
'RegionId' => 'cn-hangzhou',
'AcceleratorId' => $config['ga_id'],
'ListenerId' => $config['ga_listener_id'],
];
foreach ($need_add as $index => $domain) {
$param['Certificates.' . ($index + 1) . '.Id'] = $cert_id;
$param['Certificates.' . ($index + 1) . '.Domain'] = $domain;
}
$client->request($param);
$this->log('全球加速实例监听扩展域名 ' . implode(',', $need_add) . ' 绑定证书成功!');
}
} else {
$param = [
'Action' => 'UpdateListener',
'RegionId' => 'cn-hangzhou',
'AcceleratorId' => $config['ga_id'],
'ListenerId' => $config['ga_listener_id'],
'Certificates.1.Id' => $cert_id,
];
$client->request($param);
$this->log('全球加速实例监听默认证书更新成功!');
}
}
public function setLogger($func)
{
$this->logger = $func;

108
app/lib/deploy/amh.php Normal file
View File

@ -0,0 +1,108 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class amh implements DeployInterface
{
private $logger;
private $url;
private $apikey;
private $proxy;
public function __construct($config)
{
$this->url = rtrim($config['url'], '/');
$this->apikey = $config['apikey'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->apikey)) throw new Exception('请填写面板地址和接口密钥');
$this->login();
return true;
}
private function login()
{
$path = '/?c=amapi&a=login';
$post_data = 'amapi_expires=' . time() + 120;
$post_data .= '&amapi_sign=' . hash_hmac('sha256', $post_data, $this->apikey);
$response = $this->request($path, $post_data);
if ($response['code'] == 302 && strpos($response['redirect_url'], 'amh_token=') !== false) {
if(preg_match('/amh_token=([A-Za-z0-9]+)/', $response['redirect_url'], $matches)) {
return $matches[1];
}else{
throw new Exception('面板返回数据异常');
}
} elseif ($response['code'] == 200 && preg_match('/<p id="error".*?>(.*?)<\/p>/s', $response['body'], $matches)) {
throw new Exception(strip_tags($matches[1]));
} else {
throw new Exception('面板地址无法连接');
}
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
if (empty($config['env_name'])) throw new Exception('环境名称不能为空');
if (empty($config['vhost_name'])) throw new Exception('网站标识域名不能为空');
$amh_token = $this->login();
foreach (explode("\n", $config['vhost_name']) as $vhost_name) {
$vhost_name = trim($vhost_name);
if (empty($vhost_name)) continue;
$path = '/?c=amssl&a=admin_amssl&envs_name=' . $config['env_name'] . '&vhost_name=' . $vhost_name . '&ModuleSort=app';
$params = [
'submit_key_crt' => 'y',
'key_input1' => 'key_input1',
'key_content1' => $privatekey,
'crt_input1' => 'crt_input1',
'crt_content1' => $fullchain,
'amh_token' => $amh_token,
];
$response = $this->request($path, $params);
if (strpos($response['body'], '<p id="success"') !== false) {
$this->log("网站 {$vhost_name} 证书部署成功");
} elseif (preg_match('/<p id="error".*?>(.*?)<\/p>/s', $response['body'], $matches)) {
$errmsg = strip_tags($matches[1]);
$this->log("网站 {$vhost_name} 证书部署失败:" . $errmsg);
throw new Exception($errmsg);
} elseif (preg_match('/<p id="error".*?>(.*?)<br \/>/s', $response['body'], $matches)) {
$errmsg = $matches[1];
if (strpos($errmsg, '<br />') !== false) {
$errmsg = explode('<br />', $errmsg)[0];
}
$errmsg = strip_tags($errmsg);
$this->log("网站 {$vhost_name} 证书部署失败:" . $errmsg);
throw new Exception($errmsg);
} else {
throw new Exception("网站 {$vhost_name} 证书部署失败:未知错误");
}
}
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
private function request($path, $post_data = null)
{
$url = $this->url . $path;
$cookie = 'PHPSESSID=' . hash_hmac('md5', 'php_sessid=' . $this->apikey, $this->apikey);
$response = http_request($url, $post_data, null, $cookie, null, $this->proxy);
return $response;
}
}

View File

@ -159,14 +159,20 @@ class ssh implements DeployInterface
file_put_contents($privateKeyPath, $this->config['privatekey']);
file_put_contents($publicKeyPath, $publicKey);
umask($umask);
if (!empty($this->config['passphrase'])) {
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $this->config['passphrase'])) {
throw new Exception('私钥认证失败');
}
} else {
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) {
throw new Exception('私钥认证失败');
try {
if (!empty($this->config['passphrase'])) {
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $this->config['passphrase'])) {
throw new Exception('私钥认证失败');
}
} else {
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) {
throw new Exception('私钥认证失败');
}
}
} finally {
unlink($publicKeyPath);
unlink($privateKeyPath);
}
} else {
if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) {

View File

@ -79,6 +79,7 @@ class cloudflare implements DnsInterface
foreach ($data['result'] as $row) {
$name = $this->domain == $row['name'] ? '@' : substr($row['name'], 0, -(strlen($this->domain) + 1));
$status = str_ends_with($name, '_pause') ? '0' : '1';
$name = $name == '__root__' ? '@' : $name;
$name = $status == '0' ? substr($name, 0, -6) : $name;
if ($row['type'] == 'SRV' && isset($row['priority'])) {
$row['content'] = $row['priority'] . ' ' . $row['content'];
@ -117,6 +118,7 @@ class cloudflare implements DnsInterface
$name = $this->domain == $data['result']['name'] ? '@' : substr($data['result']['name'], 0, -(strlen($this->domain) + 1));
$status = str_ends_with($name, '_pause') ? '0' : '1';
$name = $status == '0' ? substr($name, 0, -6) : $name;
$name = $name == '__root__' ? '@' : $name;
if ($data['result']['type'] == 'SRV' && isset($data['result']['priority'])) {
$data['result']['content'] = $data['result']['priority'] . ' ' . $data['result']['content'];
}
@ -182,6 +184,12 @@ class cloudflare implements DnsInterface
{
$info = $this->getDomainRecordInfo($RecordId);
$Name = $Status == '1' ? str_replace('_pause', '', $info['Name']) : $info['Name'] . '_pause';
// @ 作为特殊字符不能设置为解析, 故设置暂停解析的时候, 替换为 __root__
if ($Name == '__root__') {
$Name = '@';
} elseif ($Name == '@_pause') {
$Name = '__root___pause';
}
return $this->updateDomainRecord($RecordId, $Name, $info['Type'], $info['Value'], $info['Line'], $info['TTL'], $info['MX'], $info['Weight'], $info['Remark']);
}

View File

@ -19,7 +19,7 @@ class OptimizeService
public static function get_license($api, $key)
{
if ($api == 2) {
throw new Exception('当前接口暂不支持');
throw new Exception('xingpingcn.top 接口免费使用,无需密钥,无积分限制');
} elseif ($api == 1) {
$url = 'https://api.hostmonit.com/get_license?license='.$key;
} else {
@ -39,7 +39,9 @@ class OptimizeService
public function get_ip_address($cdn_type = 1, $ip_type = 'v4')
{
$api = config_get('optimize_ip_api', 0);
if ($api == 1) {
if ($api == 2) {
return $this->get_ip_address_xingpingcn($ip_type);
} elseif ($api == 1) {
$url = 'https://api.hostmonit.com/get_optimization_ip';
} else {
$url = 'https://www.wetest.vip/api/cf2dns/';
@ -70,6 +72,59 @@ class OptimizeService
}
}
/**
* xingpingcn.top 获取优选IP数据
* @param string $ip_type IP类型 v4/v6
* @return array
* @throws Exception
*/
private function get_ip_address_xingpingcn($ip_type = 'v4')
{
if ($ip_type == 'v6') {
throw new Exception('xingpingcn.top 接口暂不支持IPv6');
}
$proxy = config_get('optimize_ip_proxy', '');
if (!empty($proxy)) {
$proxy = trim($proxy);
if (filter_var($proxy, FILTER_VALIDATE_URL) === false) {
throw new Exception('无效的代理地址配置URL 格式错误');
}
$scheme = parse_url($proxy, PHP_URL_SCHEME);
if (!in_array($scheme, ['http', 'https'], true)) {
throw new Exception('无效的代理地址配置:仅支持 http 和 https 协议');
}
$url = rtrim($proxy, '/') . '/xingpingcn/enhanced-FaaS-in-China/refs/heads/main/Cf.json';
} else {
$url = 'https://raw.githubusercontent.com/xingpingcn/enhanced-FaaS-in-China/refs/heads/main/Cf.json';
}
$response = get_curl($url);
if ($response === '') {
throw new Exception('获取优选IP数据失败网络请求失败请检查网络连接或代理地址');
}
$arr = json_decode($response, true);
if (isset($arr['Cf']['result'])) {
$result = $arr['Cf']['result'];
$info = [];
// 转换格式dianxin->CT, liantong->CU, yidong->CM, default->DEF
if (isset($result['dianxin']) && is_array($result['dianxin'])) {
$info['CT'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['dianxin']);
}
if (isset($result['liantong']) && is_array($result['liantong'])) {
$info['CU'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['liantong']);
}
if (isset($result['yidong']) && is_array($result['yidong'])) {
$info['CM'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['yidong']);
}
// 不使用他的默认线路数据, 因为这真的是默认. 由后续逻辑自己决定是否把CT线路当DEF来用
// if (isset($result['default']) && is_array($result['default'])) {
// $info['DEF'] = array_map(function($ip) { return ['ip' => $ip]; }, $result['default']);
// }
return $info;
} else {
throw new Exception('获取优选IP数据失败接口返回数据格式错误');
}
}
public function get_ip_address2($cdn_type = 1, $ip_type = 'v4')
{
$key = $cdn_type.'_'.$ip_type;

View File

@ -9,7 +9,7 @@
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="kw" placeholder="AccessKey或备注">
<input type="text" class="form-control" name="kw" 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>
@ -54,7 +54,7 @@ $(document).ready(function(){
},
{
field: 'name',
title: 'AccessKey'
title: '账户名称'
},
{
field: 'remark',

View File

@ -20,7 +20,7 @@
<label class="col-sm-3 control-label no-padding-right" is-required>账户类型</label>
<div class="col-sm-6">
<select name="type" class="form-control" v-model="set.type">
<option v-for="(item, key) in typeList" :value="key">{{item.name}}</option>
<option v-for="(item, key) in typeList" :value="key" :data-icon="item.icon">{{item.name}}</option>
</select>
</div>
</div>
@ -95,6 +95,8 @@
{block name="script"}
<script src="/static/js/vue-2.7.16.min.js"></script>
<script src="/static/js/layer/layer.js"></script>
<script src="/static/js/select2-4.0.13.min.js"></script>
<script src="/static/js/select2-i18n-zh-CN-4.0.13.min.js"></script>
<script src="/static/js/bootstrapValidator.min.js"></script>
<script>
var info = {$info|json_encode|raw};
@ -163,8 +165,28 @@ new Vue({
this.set.type = Object.keys(typeList)[0]
}
var that = this;
this.$nextTick(function () {
$('[data-toggle="tooltip"]').tooltip();
function formatType(option) {
if (!option.id) return option.text;
var icon = $(option.element).data('icon');
if (icon) {
return $('<span><img src="/static/images/' + icon + '" class="type-logo" />' + option.text + '</span>');
}
return option.text;
}
$('select[name=type]').select2({
templateResult: formatType,
templateSelection: formatType,
minimumResultsForSearch: Infinity,
width: '100%'
}).on('select2:select', function(e){
that.set.type = e.params.data.id;
});
if(that.action == 'edit'){
$('select[name=type]').val(that.set.type).trigger('change.select2');
}
})
},
methods: {

View File

@ -54,14 +54,14 @@
<label class="col-sm-3 control-label no-padding-right">解析IP类型<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="同时开启IPv6&IPv4将会请求2次接口消耗双倍积分"><i class="fa fa-question-circle"></i></span></label>
<div class="col-sm-6">
<label class="checkbox-inline" v-for="option in iptypeList">
<input type="checkbox" name="ip_type" :value="option.value" v-model="set.ip_type_select" required> {{option.label}}
<input type="checkbox" name="ip_type" :value="option.value" v-model="set.ip_type_select" :disabled="option.value=='v6' && isXingpingcn" required> {{option.label}}<span v-if="option.value=='v6' && isXingpingcn" class="text-muted">(xingpingcn.top不支持)</span>
</label><br/>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label no-padding-right">每线路解析数量<span class="tips" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="数量不要超过当前域名套餐允许的最大数量,否则会添加解析失败"><i class="fa fa-question-circle"></i></span></label>
<div class="col-sm-6">
<input type="text" name="recordnum" v-model="set.recordnum" placeholder="填写每线路解析数量" class="form-control" data-bv-integer="true" min="1" max="5" required>
<input type="text" name="recordnum" v-model="set.recordnum" placeholder="填写每线路解析数量" class="form-control" data-bv-integer="true" min="1" max="50" required>
</div>
</div>
<div class="form-group">
@ -110,6 +110,7 @@ new Vue({
el: '#app',
data: {
action: '{$action}',
optimize_ip_api: '{:config_get("optimize_ip_api", 0)}',
set: {
id: '',
remark: '',
@ -132,8 +133,18 @@ new Vue({
4:'EdgeOne'
},
},
computed: {
isXingpingcn: function() {
return this.optimize_ip_api == '2';
}
},
watch: {
'set.ip_type_select': function(val){
// 如果使用xingpingcn.top接口自动移除v6
if(this.isXingpingcn && val.includes('v6')){
this.set.ip_type_select = val.filter(v => v !== 'v6');
return;
}
this.set.ip_type = val.join(',');
}
},
@ -185,4 +196,4 @@ new Vue({
},
});
</script>
{/block}
{/block}

View File

@ -14,8 +14,9 @@
<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、EdgeOne<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<a href="https://github.com/xingpingcn/enhanced-FaaS-in-China" target="_blank" rel="noreferrer">xingpingcn.top</a> 只支持CloudFlare免费、无需密钥</li></p>
<p><li>接口密钥默认o1zrmHAF为免费KEY可永久免费使用。</li></p>
<p><li>代理地址:如 https://ghfast.top/https://raw.githubusercontent.com/ ,留空则直接访问 https://raw.githubusercontent.com/。</li></p>
<p><li>自动更新:可查看<a href="/system/cronset">计划任务设置</a></p>
</div>
</div>
@ -24,19 +25,23 @@
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">数据接口设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form" id="apiSettingForm">
<div class="form-group">
<label class="col-sm-3 control-label">数据接口</label>
<div class="col-sm-9"><select class="form-control" name="optimize_ip_api" default="{:config_get('optimize_ip_api')}"><option value="0">wetest.vip</option><option value="1">HostMonit</option></select></div>
<div class="col-sm-9"><select class="form-control" name="optimize_ip_api" id="optimize_ip_api" default="{:config_get('optimize_ip_api')}"><option value="0">wetest.vip</option><option value="1">HostMonit</option><option value="2">xingpingcn.top</option></select></div>
</div>
<div class="form-group">
<div class="form-group" id="keyGroup">
<label class="col-sm-3 control-label">接口密钥</label>
<div class="col-sm-9"><input type="text" name="optimize_ip_key" value="{:config_get('optimize_ip_key', 'o1zrmHAF')}" class="form-control"/></div>
</div>
<div class="form-group" id="proxyGroup" style="display:none;">
<label class="col-sm-3 control-label">代理地址</label>
<div class="col-sm-9"><input type="text" name="optimize_ip_proxy" value="{:config_get('optimize_ip_proxy', '')}" class="form-control" placeholder="留空则直接访问GitHub"/></div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
<a href="javascript:queryapi()" class="btn btn-default btn-block">查询积分</a>
<a href="javascript:queryapi()" class="btn btn-default btn-block" id="queryBtn">查询积分</a>
</div>
</div>
</form>
@ -69,6 +74,25 @@ var items = $("select[default]");
for (i = 0; i < items.length; i++) {
$(items[i]).val($(items[i]).attr("default")||0);
}
// 切换接口时显示/隐藏对应设置项
function toggleApiSettings(){
var api = $("#optimize_ip_api").val();
if(api == '2'){
$("#keyGroup").hide();
$("#proxyGroup").show();
$("#queryBtn").hide();
}else{
$("#keyGroup").show();
$("#proxyGroup").hide();
$("#queryBtn").show();
}
}
$("#optimize_ip_api").change(function(){
toggleApiSettings();
});
// 页面加载时初始化
toggleApiSettings();
$('[data-toggle="tooltip"]').tooltip();
function saveSetting(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
@ -118,4 +142,4 @@ function queryapi(){
});
}
</script>
{/block}
{/block}

12
composer.lock generated
View File

@ -8,16 +8,16 @@
"packages": [
{
"name": "cccyun/php-whois",
"version": "1.2",
"version": "1.3",
"source": {
"type": "git",
"url": "https://github.com/netcccyun/php-whois.git",
"reference": "c631f1c5e26e7150501a14cd25a2380f8a077ca1"
"reference": "f02627ba0bef005aa9e336d63541f9fd288675b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/netcccyun/php-whois/zipball/c631f1c5e26e7150501a14cd25a2380f8a077ca1",
"reference": "c631f1c5e26e7150501a14cd25a2380f8a077ca1",
"url": "https://api.github.com/repos/netcccyun/php-whois/zipball/f02627ba0bef005aa9e336d63541f9fd288675b5",
"reference": "f02627ba0bef005aa9e336d63541f9fd288675b5",
"shasum": ""
},
"require": {
@ -62,9 +62,9 @@
"црщшы"
],
"support": {
"source": "https://github.com/netcccyun/php-whois/tree/1.2"
"source": "https://github.com/netcccyun/php-whois/tree/1.3"
},
"time": "2025-06-25T06:54:23+00:00"
"time": "2026-02-12T05:56:18+00:00"
},
{
"name": "cccyun/think-captcha",

View File

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

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="812" height="812">
<rect x="0" y="0" width="812" height="812" rx="80" ry="80" fill="#333333"/>
<path d="M0,0 L136,0 L138,23 L141,47 L146,91 L152,146 L155,169 L157,188 L161,222 L163,243 L167,275 L169,294 L173,329 L176,355 L178,372 L178,377 L360,377 L354,399 L342,439 L333,470 L324,501 L321,509 L302,509 L307,504 L313,497 L313,494 L296,481 L293,481 L286,489 L277,494 L270,496 L261,496 L253,493 L248,487 L246,481 L246,474 L247,473 L276,470 L296,464 L307,458 L314,451 L318,443 L320,430 L318,420 L313,412 L306,406 L294,401 L290,400 L267,400 L254,404 L243,410 L233,419 L223,434 L217,450 L215,464 L215,478 L217,490 L222,502 L226,507 L226,509 L187,509 L193,501 L199,492 L199,488 L179,479 L176,478 L169,488 L164,493 L157,496 L150,496 L142,493 L138,488 L136,480 L138,464 L142,445 L146,435 L151,429 L157,426 L162,425 L169,425 L176,428 L181,436 L182,439 L187,438 L208,430 L207,422 L201,412 L193,406 L182,401 L175,400 L155,400 L143,404 L132,410 L122,419 L115,430 L110,440 L107,450 L105,468 L106,488 L111,500 L116,509 L86,509 L84,496 L81,460 L78,432 L74,385 L-27,385 L-22,373 L-7,342 L1,324 L9,308 L14,297 L67,297 L66,293 L62,236 L60,213 L57,169 L52,104 L52,95 L45,95 L43,101 L27,136 L11,171 L-7,210 L-26,251 L-42,285 L-50,304 L-60,326 L-68,342 L-76,359 L-84,375 L-96,400 L-114,437 L-125,460 L-137,485 L-148,508 L-149,509 L-261,509 L-259,503 L-245,475 L-235,456 L-212,411 L-202,392 L-189,366 L-179,347 L-159,308 L-145,281 L-137,266 L-129,250 L-105,203 L-91,176 L-82,159 L-74,143 L-51,98 L-34,65 L-19,36 L-1,1 Z " fill="#FEFEFE" transform="translate(372,151)"/>
<path d="M0,0 L11,0 L17,3 L19,6 L19,15 L14,21 L4,25 L-14,28 L-20,28 L-18,17 L-13,9 L-5,2 Z " fill="#FBFBFB" transform="translate(642,575)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB