15 Commits

Author SHA1 Message Date
net909
d0eb096873 腾讯云支持更新证书内容接口 2025-12-25 11:49:07 +08:00
net909
ebdc34cf4b fix: 又拍云SSL不兼容的特化处理 2025-12-25 10:27:28 +08:00
耗子
b19cabcbfd fix: Passing null to parameter #5 ($passphrase) of type string is deprecated (#360) 2025-12-24 22:09:48 +08:00
深山大柠檬
64b5221787 1panel支持多个子节点部署 (#356) 2025-12-18 11:23:06 +08:00
net909
41e719720c version 2025-12-16 20:16:54 +08:00
net909
16a9c03b6c 修复部署到阿里云WAF 2025-12-16 20:16:30 +08:00
net909
1beb731a6e 增加域名列表排序,批量刷新到期时间 2025-12-13 23:31:47 +08:00
dependabot[bot]
6b026ce4e4 Bump phpmailer/phpmailer from 6.11.1 to 7.0.1 (#350)
Bumps [phpmailer/phpmailer](https://github.com/PHPMailer/PHPMailer) from 6.11.1 to 7.0.1.
- [Release notes](https://github.com/PHPMailer/PHPMailer/releases)
- [Changelog](https://github.com/PHPMailer/PHPMailer/blob/master/changelog.md)
- [Commits](https://github.com/PHPMailer/PHPMailer/compare/v6.11.1...v7.0.1)

---
updated-dependencies:
- dependency-name: phpmailer/phpmailer
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-13 23:26:47 +08:00
耗子
96ff262333 feat: ssh私钥支持设置密码 (#346) 2025-11-11 10:36:19 +08:00
net909
17ffe5704f 1panel支持子节点部署 2025-11-08 16:05:56 +08:00
mrdong916
d4c11b520f 新增Spaceship DNS (#335)
Co-authored-by: mrdong916 <mrdong916@gmail.com>
2025-11-08 16:03:15 +08:00
net909
ba418da84c fix: 证书私钥 EC 指示 2025-11-04 20:42:52 +08:00
TomyJan
b58db855ca fix: 证书私钥 EC 指示 2025-11-04 18:57:55 +08:00
dependabot[bot]
3bd45367b0 Bump symfony/yaml from 7.3.3 to 7.3.5 (#339)
Bumps [symfony/yaml](https://github.com/symfony/yaml) from 7.3.3 to 7.3.5.
- [Release notes](https://github.com/symfony/yaml/releases)
- [Changelog](https://github.com/symfony/yaml/blob/7.3/CHANGELOG.md)
- [Commits](https://github.com/symfony/yaml/compare/v7.3.3...v7.3.5)

---
updated-dependencies:
- dependency-name: symfony/yaml
  dependency-version: 7.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 20:40:10 +08:00
dependabot[bot]
a22bc4fa37 Bump symfony/var-dumper from 7.3.4 to 7.3.5 (#340)
Bumps [symfony/var-dumper](https://github.com/symfony/var-dumper) from 7.3.4 to 7.3.5.
- [Release notes](https://github.com/symfony/var-dumper/releases)
- [Changelog](https://github.com/symfony/var-dumper/blob/7.3/CHANGELOG.md)
- [Commits](https://github.com/symfony/var-dumper/compare/v7.3.4...v7.3.5)

---
updated-dependencies:
- dependency-name: symfony/var-dumper
  dependency-version: 7.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 20:39:46 +08:00
20 changed files with 784 additions and 51 deletions

View File

@@ -182,6 +182,7 @@ class Domain extends BaseController
$kw = input('post.kw', null, 'trim');
$type = input('post.type', null, 'trim');
$status = input('post.status', null, 'trim');
$order = input('post.order', null, 'trim');
$offset = input('post.offset/d', 0);
$limit = input('post.limit/d', 10);
@@ -203,7 +204,23 @@ class Domain extends BaseController
}
}
$total = $select->count();
$rows = $select->fieldRaw('A.*,B.type,B.remark aremark')->order('A.id', 'desc')->limit($offset, $limit)->select();
switch ($order) {
case '1':
$select->order('A.regtime', 'asc');
break;
case '2':
$select->order('A.regtime', 'desc');
break;
case '3':
$select->order('A.expiretime', 'asc');
break;
case '4':
$select->order('A.expiretime', 'desc');
break;
default:
$select->order('A.id', 'desc');
}
$rows = $select->fieldRaw('A.*,B.type,B.remark aremark')->limit($offset, $limit)->select();
$list = [];
foreach ($rows as $row) {
@@ -321,6 +338,12 @@ class Domain extends BaseController
Db::name('optimizeip')->where('did', 'in', $ids)->delete();
Db::name('sctask')->where('did', 'in', $ids)->delete();
return json(['code' => 0, 'msg' => '成功删除' . count($ids) . '个域名!']);
} elseif ($act == 'updateexpire') {
if (!checkPermission(2)) return $this->alert('error', '无权限');
$ids = input('post.ids');
if (empty($ids)) return json(['code' => -1, 'msg' => '参数不能为空']);
$count = Db::name('domain')->where('id', 'in', $ids)->update(['checkstatus' => 0]);
return json(['code' => 0, 'msg' => '已提交' . $count . '个域名,约' . ceil($count / 5) . '分钟后刷新完成。']);
}
return json(['code' => -3]);
}

View File

@@ -64,7 +64,7 @@ class Index extends BaseController
'framework_version' => app()->version(),
'php_version' => PHP_VERSION,
'mysql_version' => $mysqlVersion,
'software' => $_SERVER['SERVER_SOFTWARE'],
'software' => $_SERVER['SERVER_SOFTWARE'] ?? '未知',
'os' => php_uname(),
'date' => date("Y-m-d H:i:s"),
];

View File

@@ -559,7 +559,7 @@ class DeployHelper
'icon' => 'opanel.png',
'desc' => '更新面板证书管理内的SSL证书',
'note' => null,
'tasknote' => '系统会根据关联SSL证书的域名自动更新对应证书',
'tasknote' => '',
'inputs' => [
'url' => [
'name' => '面板地址',
@@ -581,7 +581,7 @@ class DeployHelper
'v1' => '1.x',
'v2' => '2.x',
],
'value' => 'v1',
'value' => 'v2',
'required' => true,
],
'proxy' => [
@@ -594,7 +594,32 @@ class DeployHelper
'value' => '0'
],
],
'taskinputs' => [],
'taskinputs' => [
'type' => [
'name' => '部署类型',
'type' => 'radio',
'options' => [
'0' => '更新已有证书',
'3' => '面板本身的证书',
],
'value' => '0',
'required' => true,
],
'id' => [
'name' => '证书ID',
'type' => 'input',
'placeholder' => '在证书列表查看ID',
'note' => '留空为根据关联SSL证书的域名自动更新对应证书',
'show' => 'type==0',
],
'node_name' => [
'name' => '子节点名称',
'type' => 'textarea',
'placeholder' => '每行一个节点名称',
'note' => '不填写时,将替换主控节点证书;否则,将替换被控节点证书。多个节点请每行填写一个',
'show' => 'type==0',
],
],
],
'mwpanel' => [
'name' => 'MW面板',
@@ -1199,6 +1224,7 @@ ctrl+x 保存退出',
['value'=>'tse', 'label'=>'云原生API网关TSE'],
['value'=>'tcb', 'label'=>'云开发TCB'],
['value'=>'lighthouse', 'label'=>'轻量应用服务器'],
['value'=>'update', 'label'=>'更新证书内容证书ID不变'],
],
'value' => 'cdn',
'required' => true,
@@ -1302,6 +1328,14 @@ ctrl+x 保存退出',
'note' => 'CDN、EO、WAF多个域名可用,隔开其他只能填写1个域名',
'required' => true,
],
'cert_id' => [
'name' => '证书ID',
'type' => 'input',
'placeholder' => '要更新的证书ID在我的证书列表查看',
'show' => 'product==\'update\'',
'required' => true,
'note' => '当前接口需联系加白使用',
],
],
],
'huawei' => [
@@ -2248,6 +2282,12 @@ ctrl+x 保存退出',
'required' => true,
'show' => 'auth==1',
],
'passphrase' => [
'name' => '私钥密码',
'type' => 'input',
'placeholder' => '若私钥有设置密码,请填写此项',
'show' => 'auth==1',
],
'windows' => [
'name' => '是否Windows',
'type' => 'radio',

View File

@@ -162,6 +162,20 @@ class DnsHelper
'page' => true,
'add' => true,
],
'spaceship' => [
'name' => 'Spaceship',
'config' => [
'ak' => 'AccessKey',
'sk' => 'SecretKey',
],
'remark' => 0,
'status' => false,
'redirect' => true,
'log' => false,
'weight' => false,
'page' => false,
'add' => true,
],
];
public static $line_name = [
@@ -176,6 +190,7 @@ class DnsHelper
'cloudflare' => ['DEF' => '0'],
'namesilo' => ['DEF' => 'default'],
'powerdns' => ['DEF' => 'default'],
'spaceship' => ['DEF' => 'default'],
];
public static function getList()

View File

@@ -311,7 +311,20 @@ class aliyun implements DeployInterface
}
$data['Listen']['CertId'] = $cert_id;
if (empty($data['Listen']['HttpsPorts'])) $data['Listen']['HttpsPorts'] = [443];
if (empty($data['Listen']['HttpsPorts'])) {
$data['Listen']['HttpsPorts'] = [443];
$data['Listen']['TLSVersion'] = 'tlsv1.1';
$data['Listen']['EnableTLSv3'] = true;
$data['Listen']['CipherSuite'] = 1;
}
if (count($data['Redirect']['BackendPorts']) == 1 && $data['Redirect']['BackendPorts'][0]['Protocol'] == 'http') {
$data['Redirect']['BackendPorts'][] = [
'ListenPort' => 443,
'Protocol' => 'https',
'BackendPort' => $data['Redirect']['BackendPorts'][0]['BackendPort'],
];
$data['Redirect']['FocusHttpBackend'] = true;
}
$data['Redirect']['Backends'] = $data['Redirect']['AllBackends'];
$param = [
'Action' => 'ModifyDomain',

View File

@@ -27,15 +27,108 @@ class opanel implements DeployInterface
public function deploy($fullchain, $privatekey, $config, &$info)
{
// 解析节点名称列表
$nodeNames = $this->parseNodeNames($config);
if (isset($config['type']) && $config['type'] == '3') {
// 面板本身的证书部署
$params = [
'cert' => $fullchain,
'key' => $privatekey,
'ssl' => 'Enable',
'sslID' => null,
'sslType' => 'import-paste',
];
if (empty($nodeNames)) {
// 没有指定节点,部署到主控节点
try {
$this->request('/core/settings/ssl/update', $params);
$this->log("面板证书更新成功!");
return;
} catch (Exception $e) {
throw new Exception("面板证书更新失败:" . $e->getMessage());
}
} else {
// 部署到多个子节点
$successCount = 0;
$failCount = 0;
foreach ($nodeNames as $nodeName) {
try {
$this->request('/core/settings/ssl/update', $params, $nodeName);
$this->log("节点 [{$nodeName}] 面板证书更新成功!");
$successCount++;
} catch (Exception $e) {
$this->log("节点 [{$nodeName}] 面板证书更新失败:" . $e->getMessage());
$failCount++;
}
}
if ($failCount > 0 && $successCount == 0) {
throw new Exception("所有节点证书更新失败");
}
return;
}
}
// 如果没有指定节点,则部署到主控节点
if (empty($nodeNames)) {
$this->deployToNode($fullchain, $privatekey, $config, null);
} else {
// 部署到多个子节点
$successCount = 0;
$failCount = 0;
foreach ($nodeNames as $nodeName) {
try {
$this->deployToNode($fullchain, $privatekey, $config, $nodeName);
$successCount++;
} catch (Exception $e) {
$this->log("节点 [{$nodeName}] 部署失败:" . $e->getMessage());
$failCount++;
}
}
if ($failCount > 0 && $successCount == 0) {
throw new Exception("所有节点部署失败");
}
}
}
/**
* 部署到指定节点
*/
private function deployToNode($fullchain, $privatekey, $config, $nodeName = null)
{
if (!empty($config['id'])) {
// 指定证书ID的情况
$params = [
'sslID' => intval($config['id']),
'type' => 'paste',
'certificate' => $fullchain,
'privateKey' => $privatekey,
'description' => '',
];
try {
$this->request('/websites/ssl/upload', $params, $nodeName);
$logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$config['id']}更新成功!" : "证书ID:{$config['id']}更新成功!";
$this->log($logMsg);
return;
} catch (Exception $e) {
$logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$config['id']}更新失败:" : "证书ID:{$config['id']}更新失败:";
throw new Exception($logMsg . $e->getMessage());
}
}
// 根据域名自动匹配证书
$domains = $config['domainList'];
if (empty($domains)) throw new Exception('没有设置要部署的域名');
$params = ['page' => 1, 'pageSize' => 500];
try {
$data = $this->request("/websites/ssl/search", $params);
$this->log('获取证书列表成功(total=' . $data['total'] . ')');
$data = $this->request("/websites/ssl/search", $params, $nodeName);
$logMsg = $nodeName ? "节点 [{$nodeName}] " : "";
$this->log($logMsg . '获取证书列表成功(total=' . $data['total'] . ')');
} catch (Exception $e) {
throw new Exception('获取证书列表失败:' . $e->getMessage());
$logMsg = $nodeName ? "节点 [{$nodeName}] " : "";
throw new Exception($logMsg . '获取证书列表失败:' . $e->getMessage());
}
$success = 0;
@@ -62,18 +155,29 @@ class opanel implements DeployInterface
'description' => '',
];
try {
$this->request('/websites/ssl/upload', $params);
$this->log("证书ID:{$row['id']}更新成功!");
$this->request('/websites/ssl/upload', $params, $nodeName);
$logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$row['id']}更新成功!" : "证书ID:{$row['id']}更新成功!";
$this->log($logMsg);
$success++;
} catch (Exception $e) {
$errmsg = $e->getMessage();
$this->log("证书ID:{$row['id']}更新失败:" . $errmsg);
$logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$row['id']}更新失败:" : "证书ID:{$row['id']}更新失败:";
$this->log($logMsg . $errmsg);
}
}
}
}
if ($success == 0) {
throw new Exception($errmsg ? $errmsg : '没有要更新的证书');
$params = [
'sslID' => 0,
'type' => 'paste',
'certificate' => $fullchain,
'privateKey' => $privatekey,
'description' => '',
];
$this->request('/websites/ssl/upload', $params, $nodeName);
$logMsg = $nodeName ? "节点 [{$nodeName}] 证书上传成功!" : "证书上传成功!";
$this->log($logMsg);
}
}
@@ -89,7 +193,32 @@ class opanel implements DeployInterface
}
}
private function request($path, $params = null)
/**
* 解析节点名称列表
*/
private function parseNodeNames($config)
{
if (!isset($config['node_name']) || empty($config['node_name'])) {
return [];
}
$nodeNameStr = trim($config['node_name']);
if (empty($nodeNameStr)) {
return [];
}
// 按行分割,过滤空行
$nodeNames = array_filter(
array_map('trim', explode("\n", $nodeNameStr)),
function($name) {
return !empty($name);
}
);
return array_values($nodeNames);
}
private function request($path, $params = null, $nodeName = null)
{
$url = $this->url . $path;
@@ -97,8 +226,11 @@ class opanel implements DeployInterface
$token = md5('1panel' . $this->key . $timestamp);
$headers = [
'1Panel-Token' => $token,
'1Panel-Timestamp' => $timestamp
'1Panel-Timestamp' => $timestamp,
];
if (!empty($nodeName)) {
$headers['CurrentNode'] = $nodeName;
}
$body = $params ? json_encode($params) : '{}';
if ($body) $headers['Content-Type'] = 'application/json';
$response = http_request($url, $body, null, null, $headers, $this->proxy);

View File

@@ -2,6 +2,7 @@
namespace app\lib\deploy;
use app\lib\CertHelper;
use app\lib\DeployInterface;
use Exception;
@@ -49,7 +50,8 @@ class ssh implements DeployInterface
fclose($stream);
$this->log('私钥已保存到:' . $config['pem_key_file']);
} elseif ($config['format'] == 'pfx') {
$pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null);
$pfx_pass = $config['pfx_pass'] ?? null;
$pfx = CertHelper::getPfx($fullchain, $privatekey, $pfx_pass);
$stream = fopen("ssh2.sftp://$sftp{$config['pfx_file']}", 'w');
if (!$stream) {
@@ -157,8 +159,14 @@ class ssh implements DeployInterface
file_put_contents($privateKeyPath, $this->config['privatekey']);
file_put_contents($publicKeyPath, $publicKey);
umask($umask);
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) {
throw new Exception('私钥认证失败');
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('私钥认证失败');
}
}
} else {
if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) {

View File

@@ -31,6 +31,9 @@ class tencent implements DeployInterface
public function deploy($fullchain, $privatekey, $config, &$info)
{
if ($config['product'] == 'update') {
return $this->update_cert($fullchain, $privatekey, $config);
}
$cert_id = $this->get_cert_id($fullchain, $privatekey);
if (!$cert_id) throw new Exception('证书ID获取失败');
if ($config['product'] == 'cos') {
@@ -281,6 +284,95 @@ class tencent implements DeployInterface
$this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!');
}
private function update_cert($fullchain, $privatekey, $config)
{
if (empty($config['cert_id'])) throw new Exception('证书ID不能为空');
$param = [
'CertificateIds' => [$config['cert_id']],
'IsCache' => 1,
];
try {
$data = $this->client->request('CreateCertificateBindResourceSyncTask', $param);
if (empty($data['CertTaskIds'])) throw new Exception('返回任务ID为空');
} catch (Exception $e) {
throw new Exception('创建关联云资源查询任务失败:' . $e->getMessage());
}
$task_id = $data['CertTaskIds'][0]['TaskId'];
$this->log('创建关联云资源查询任务成功 TaskId=' . $task_id);
$retry = 0;
$resource_result = null;
while ($retry++ < 30) {
sleep(2);
$param = [
'TaskIds' => [$task_id],
];
try {
$data = $this->client->request('DescribeCertificateBindResourceTaskResult', $param);
if (empty($data['SyncTaskBindResourceResult'])) throw new Exception('返回结果为空');
} catch (Exception $e) {
throw new Exception('查询关联云资源任务结果失败:' . $e->getMessage());
}
$taskResult = $data['SyncTaskBindResourceResult'][0];
if ($taskResult['Status'] == 1) {
$resource_result = $taskResult['BindResourceResult'];
break;
} elseif ($taskResult['Status'] == 2) {
throw new Exception('关联云资源查询任务执行失败:' . isset($taskResult['Error']) ? $taskResult['Error']['Message'] : '未知错误');
}
};
if (!$resource_result) {
throw new Exception('关联云资源查询任务超时未完成,请稍后重试');
}
$resourceTypes = [];
$resourceTypesRegions = [];
foreach ($resource_result as $res) {
if ($res['ResourceType'] != 'clb') continue;
$totalCount = 0;
$regions = [];
foreach ($res['BindResourceRegionResult'] as $regionRes) {
if ($regionRes['TotalCount'] > 0) {
$totalCount += $regionRes['TotalCount'];
if (!empty($regionRes['Region'])) {
$regions[] = $regionRes['Region'];
}
}
}
if ($totalCount > 0) {
$resourceTypes[] = $res['ResourceType'];
if (!empty($regions)) {
$resourceTypesRegions[] = [
'ResourceType' => $res['ResourceType'],
'Regions' => $regions,
];
}
}
}
$param = [
'OldCertificateId' => $config['cert_id'],
'CertificatePublicKey' => $fullchain,
'CertificatePrivateKey' => $privatekey,
'ResourceTypes' => $resourceTypes,
'ResourceTypesRegions' => $resourceTypesRegions,
];
$retry = 0;
while ($retry++ < 10) {
try {
$data = $this->client->request('UploadUpdateCertificateInstance', $param);
} catch (Exception $e) {
throw new Exception('更新证书内容失败:' . $e->getMessage());
}
if ($data['DeployStatus'] == 1) {
break;
}
sleep(1);
}
$this->log('更新证书内容成功,可能需要一些时间完成各资源的证书更新部署');
}
public function setLogger($func)
{
$this->logger = $func;

View File

@@ -31,9 +31,15 @@ class upyun implements DeployInterface
$this->login();
$url = 'https://console.upyun.com/api/https/certificate/';
// 如果是 EC 证书,调整私钥头为 EC PRIVATE KEY
$privatekey_send = $privatekey;
if ($this->isEcCertificate($fullchain)) {
$privatekey_send = str_replace('-----BEGIN PRIVATE KEY-----', '-----BEGIN EC PRIVATE KEY-----', $privatekey_send);
$privatekey_send = str_replace('-----END PRIVATE KEY-----', '-----END EC PRIVATE KEY-----', $privatekey_send);
}
$params = [
'certificate' => $fullchain,
'private_key' => $privatekey,
'private_key' => $privatekey_send,
];
$response = http_request($url, http_build_query($params), null, $this->cookie, null, $this->proxy);
$result = json_decode($response['body'], true);
@@ -130,4 +136,22 @@ class upyun implements DeployInterface
call_user_func($this->logger, $txt);
}
}
/**
* 判断是否为 EC (ECDSA) 证书
*/
private function isEcCertificate($fullchain)
{
// 提取第一个证书
if (!preg_match('/-----BEGIN CERTIFICATE-----\s*(.+?)\s*-----END CERTIFICATE-----/s', $fullchain, $m)) {
return false;
}
$pubKey = openssl_pkey_get_public($m[0]);
if (!$pubKey) return false;
$details = openssl_pkey_get_details($pubKey);
return $details && ($details['type'] ?? 0) === OPENSSL_KEYTYPE_EC;
}
}

View File

@@ -78,6 +78,9 @@ class cloudflare implements DnsInterface
$name = $this->domain == $row['name'] ? '@' : str_replace('.'.$this->domain, '', $row['name']);
$status = str_ends_with($name, '_pause') ? '0' : '1';
$name = $status == '0' ? substr($name, 0, -6) : $name;
if ($row['type'] == 'SRV' && isset($row['priority'])) {
$row['content'] = $row['priority'] . ' ' . $row['content'];
}
$list[] = [
'RecordId' => $row['id'],
'Domain' => $this->domain,
@@ -112,6 +115,9 @@ class cloudflare implements DnsInterface
$name = $this->domain == $data['result']['name'] ? '@' : str_replace('.' . $this->domain, '', $data['result']['name']);
$status = str_ends_with($name, '_pause') ? '0' : '1';
$name = $status == '0' ? substr($name, 0, -6) : $name;
if ($data['result']['type'] == 'SRV' && isset($data['result']['priority'])) {
$data['result']['content'] = $data['result']['priority'] . ' ' . $data['result']['content'];
}
return [
'RecordId' => $data['result']['id'],
'Domain' => $this->domain,
@@ -257,7 +263,7 @@ class cloudflare implements DnsInterface
{
$url = $this->baseUrl . $path;
if (preg_match('/^[0-9a-z]+$/i', $this->ApiKey)) {
if (preg_match('/^[0-9a-f]+$/i', $this->ApiKey)) {
$headers = [
'X-Auth-Email: ' . $this->Email,
'X-Auth-Key: ' . $this->ApiKey,

353
app/lib/dns/spaceship.php Normal file
View File

@@ -0,0 +1,353 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
/**
* @see https://docs.spaceship.dev/
*/
class spaceship implements DnsInterface
{
private $apiKey;
private $apiSecret;
private $baseUrl = 'https://spaceship.dev/api/v1';
private $error;
private $domain;
private $proxy;
public function __construct($config)
{
$this->apiKey = $config['ak'];
$this->apiSecret = $config['sk'];
$this->domain = $config['domain'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function getError()
{
return $this->error;
}
public function check()
{
if ($this->getDomainList() != false) {
return true;
}
return false;
}
//获取域名列表
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 100)
{
$param = ['take' => $PageSize, 'skip' => ($PageNumber - 1) * $PageSize];
$data = $this->send_reuqest('GET', '/domains', $param);
if ($data) {
$list = [];
foreach ($data['items'] as $row) {
$list[] = [
'DomainId' => $row['name'],
'Domain' => $row['name'],
'RecordCount' => 0,
];
}
return ['total' => $data['total'], 'list' => $list];
}
return false;
}
//获取解析记录列表
private function send_reuqest($method, $path, $params = null)
{
$url = $this->baseUrl . $path;
$headers = [
'X-API-Key: ' . $this->apiKey,
'X-API-Secret: ' . $this->apiSecret,
];
$body = '';
if ($method == 'GET') {
if ($params) {
$url .= '?' . http_build_query($params);
}
} else {
$body = json_encode($params);
$headers[] = 'Content-Type: application/json';
}
$ch = curl_init($url);
if ($this->proxy) {
curl_set_proxy($ch);
}
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
if ($method == 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'PATCH') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$this->setError('Curl error: ' . curl_error($ch));
}
curl_close($ch);
if ($errno) return false;
$arr = json_decode($response, true);
if (!isset($arr['detail'])) {
return $arr;
} else {
$this->setError($response['detail']);
return false;
}
}
//获取子域名解析记录列表
private function setError($message)
{
$this->error = $message;
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
}
//获取解析记录详细信息
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
{
if ($SubDomain == '') $SubDomain = '@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
}
//添加解析记录
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$param = ['take' => $PageSize, 'skip' => ($PageNumber - 1) * $PageSize];
if (!isNullOrEmpty(($SubDomain))) {
$param['host'] = $SubDomain;
}
$data = $this->send_reuqest('GET', '/dns/records/' . $this->domain, $param);
if ($data) {
$list = [];
foreach ($data['items'] as $row) {
$type = $row['type'];
$name = $row['name'];
if ('MX' == $type) {
$address = $row['exchange'];
$mx = $row['preference'];
} else if ('CNAME' == $type) {
$address = $row['cname'];
$mx = 0;
} else if ('TXT' == $type) {
$address = $row['value'];
$mx = 0;
} else if ('PTR' == $type) {
$address = $row['pointer'];
$mx = 0;
} else if ('NS' == $type) {
$address = $row['nameserver'];
$mx = 0;
} else if ('HTTPS' == $type) {
$address = $row['targetName'] . $row['svcParams'] . '|' . $row['svcPriority'];
$mx = 0;
} else if ('CAA' == $type) {
$address = $row['value'];
$mx = 0;
} else if ('TLSA' == $type) {
$address = $row['associationData'];
$mx = 0;
} else if ('SVRB' == $type) {
$address = $row['targetName'] . $row['svcParams'] . '|' . $row['svcPriority'];
$mx = 0;
} else if ('ALIAS' == $type) {
$address = $row['aliasName'];
$mx = 0;
} else {
$address = $row['address'];
$mx = 0;
}
$list[] = [
'RecordId' => $row['type'] . '|' . $name . '|' . $address . '|' . $mx,
'Domain' => $this->domain,
'Name' => $row['name'],
'Type' => $row['type'],
'Value' => $address,
'TTL' => $row['ttl'],
'Line' => 'default',
'MX' => $mx,
'Status' => '1',
'Weight' => null,
'Remark' => null,
'UpdateTime' => null,
];
}
return ['total' => $data['total'], 'list' => $list];
}
return false;
}
//修改解析记录
public function getDomainRecordInfo($RecordId)
{
return false;
}
//修改解析记录备注
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$param = [
'force' => true,
'items' => [
[
'type' => $this->convertType($Type),
'name' => $Name,
'address' => $Value,
'ttl' => $TTL,
]
]
];
$data = $this->send_reuqest('PUT', '/dns/records/' . $this->domain, $param);
return !isset($data);
}
//删除解析记录
private function convertType($type)
{
return $type;
}
//设置解析记录状态
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$param = [
'force' => true,
'items' => [
[
'type' => $this->convertType($Type),
'name' => $Name,
'address' => $Value,
'ttl' => $TTL,
]
]
];
$data = $this->send_reuqest('PUT', '/dns/records/' . $this->domain, $param);
return !isset($data);
}
//获取解析记录操作日志
public function updateDomainRecordRemark($RecordId, $Remark)
{
return false;
}
//获取解析线路列表
public function deleteDomainRecord($RecordId)
{
$array = explode("|", $RecordId);
$type = $array[0];
$name = $array[1];
$address = $array[2];
$mx = $array[3];
if ('MX' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'exchange' => $address,
'preference' => (int)$mx,
]
];
} else if ('TXT' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'value' => $address,
]
];
} else if ('CNAME' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'cname' => $address,
]
];
} else if ('ALIAS' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'aliasName' => $address,
]
];
} else {
$param = [
[
'type' => $type,
'name' => $name,
'address' => $address,
]
];
}
$data = $this->send_reuqest('DELETE', '/dns/records/' . $this->domain, $param);
return !isset($data);
}
//获取域名信息
public function setDomainRecordStatus($RecordId, $Status)
{
return false;
}
//获取域名最低TTL
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
{
return false;
}
public function getRecordLine()
{
return ['default' => ['name' => '默认', 'parent' => null]];
}
public function getDomainInfo()
{
return false;
}
public function getMinTTL()
{
return false;
}
public function addDomain($Domain)
{
return false;
}
}

View File

@@ -48,7 +48,7 @@ class ExpireNoticeService
private function refreshDomainList()
{
$domainList = Db::name('domain')->field('id,name')->where('expiretime', null)->where('checkstatus', 0)->select();
$domainList = Db::name('domain')->field('id,name')->where('checkstatus', 0)->select();
$count = 0;
foreach ($domainList as $domain) {
$res = $this->updateDomainDate($domain['id'], $domain['name']);

View File

@@ -150,7 +150,7 @@
{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/bootstrapValidator.min.js"></script>
<script src="/static/js/bootstrapValidator.min.js?v=2"></script>
<script>
var action = '{$action}';
var info = {$info|json_encode|raw};

View File

@@ -139,12 +139,15 @@
<div class="form-group">
<select name="status" class="form-control"><option value="">所有状态</option><option value="1">即将到期</option><option value="2">已到期</option></select>
</div>
<div class="form-group">
<select name="order" class="form-control"><option value="">默认排序</option><option value="1">注册时间↑</option><option value="2">注册时间↓</option><option value="3">到期时间↑</option><option value="4">到期时间↓</option></select>
</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>
{if request()->user['level'] eq 2}<a href="javascript:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">批量操作 <span class="caret"></span></button>
<ul class="dropdown-menu"><li><a href="/domain/add">添加域名</a></li><li><a href="javascript:operation('editremark')">修改域名备注</a></li><li><a href="javascript:operation('opennotice')">开启到期提醒</a></li><li><a href="javascript:operation('closenotice')">关闭到期提醒</a></li><li><a href="javascript:operation('delete')">删除域名</a></li><li role="separator" class="divider"></li><li><a href="javascript:operation('addrecord')">添加解析</a></li><li><a href="javascript:operation('editrecord')">修改解析</a></li></ul>
<ul class="dropdown-menu"><li><a href="/domain/add">添加域名</a></li><li><a href="javascript:operation('editremark')">修改域名备注</a></li><li><a href="javascript:operation('opennotice')">开启到期提醒</a></li><li><a href="javascript:operation('closenotice')">关闭到期提醒</a></li><li><a href="javascript:operation('updateexpire')">刷新到期时间</a></li><li><a href="javascript:operation('delete')">删除域名</a></li><li role="separator" class="divider"></li><li><a href="javascript:operation('addrecord')">添加解析</a></li><li><a href="javascript:operation('editrecord')">修改解析</a></li></ul>
</div>
<a href="/domain/expirenotice" class="btn btn-default">到期提醒设置</a>{/if}
</form>
@@ -535,6 +538,30 @@ function operation(action){
}, function(){
layer.close(confirmobj);
});
}else if(action == 'updateexpire'){
var confirmobj = layer.confirm('提交后将异步刷新所选域名的到期时间', {
btn: ['确定','取消']
}, function(){
var ii = layer.load(2);
$.ajax({
type : 'POST',
url : '/domain/op/act/updateexpire',
data : {ids: ids},
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.closeAll();
layer.alert(data.msg, {icon: 1});
searchRefresh();
}else{
layer.alert(data.msg, {icon: 2});
}
}
});
}, function(){
layer.close(confirmobj);
});
}else{
var is_notice = action == 'opennotice' ? 1 : 0;
var ii = layer.load(2);

View File

@@ -106,7 +106,7 @@
<div class="form-group" v-show="set.switchtype==0">
<label class="col-sm-3 control-label no-padding-right" is-required>记录值</label>
<div class="col-sm-6">
<input type="text" name="value" v-model="set.value" placeholder="支持填写IPv4或CNAME地址" class="form-control" required>
<input type="text" name="value" v-model="set.value" placeholder="支持填写IP或CNAME地址" class="form-control" required>
</div>
</div>
<div class="form-group" v-show="set.switchtype==0&&dnstype=='cloudflare'">

View File

@@ -53,7 +53,7 @@
"cccyun/php-whois": "^1.0",
"cccyun/think-captcha": "^3.0",
"guzzlehttp/guzzle": "^7.0",
"phpmailer/phpmailer": "^6.10",
"phpmailer/phpmailer": "^7.0",
"symfony/polyfill-intl-idn": "^1.32",
"symfony/polyfill-mbstring": "^1.32",
"symfony/polyfill-php81": "^1.32",

48
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f7c4abfaf4cb80cd99107e9e1763e75c",
"content-hash": "34b2ff614d9cf3cc515823086a4f091b",
"packages": [
{
"name": "cccyun/php-whois",
@@ -445,16 +445,16 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v6.11.1",
"version": "v7.0.1",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284"
"reference": "360ae911ce62e25e11249f6140fa58939f556ebe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d9e3b36b47f04b497a0164c5a20f92acb4593284",
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/360ae911ce62e25e11249f6140fa58939f556ebe",
"reference": "360ae911ce62e25e11249f6140fa58939f556ebe",
"shasum": ""
},
"require": {
@@ -468,13 +468,13 @@
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.7.2",
"phpcompatibility/php-compatibility": "^10.0.0@dev",
"squizlabs/php_codesniffer": "^3.13.5",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
@@ -515,7 +515,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.11.1"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.0.1"
},
"funding": [
{
@@ -523,7 +523,7 @@
"type": "github"
}
],
"time": "2025-09-30T11:54:53+00:00"
"time": "2025-11-25T07:18:09+00:00"
},
{
"name": "psr/container",
@@ -1452,16 +1452,16 @@
},
{
"name": "symfony/yaml",
"version": "v7.3.3",
"version": "v7.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d"
"reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d",
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d",
"url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc",
"reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc",
"shasum": ""
},
"require": {
@@ -1504,7 +1504,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v7.3.3"
"source": "https://github.com/symfony/yaml/tree/v7.3.5"
},
"funding": [
{
@@ -1524,7 +1524,7 @@
"type": "tidelift"
}
],
"time": "2025-08-27T11:34:33+00:00"
"time": "2025-09-27T09:00:46+00:00"
},
{
"name": "topthink/framework",
@@ -1907,16 +1907,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v7.3.4",
"version": "v7.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb"
"reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d",
"reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d",
"shasum": ""
},
"require": {
@@ -1970,7 +1970,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.3.4"
"source": "https://github.com/symfony/var-dumper/tree/v7.3.5"
},
"funding": [
{
@@ -1990,7 +1990,7 @@
"type": "tidelift"
}
],
"time": "2025-09-11T10:12:26+00:00"
"time": "2025-09-27T09:00:46+00:00"
},
{
"name": "topthink/think-trace",
@@ -2046,7 +2046,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
@@ -2060,6 +2060,6 @@
"ext-sockets": "*",
"ext-ssh2": "*"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long