20 Commits

Author SHA1 Message Date
dependabot[bot]
c73f9cd536 Bump symfony/polyfill-mbstring from 1.36.0 to 1.37.0 (#450)
Bumps [symfony/polyfill-mbstring](https://github.com/symfony/polyfill-mbstring) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/symfony/polyfill-mbstring/releases)
- [Commits](https://github.com/symfony/polyfill-mbstring/compare/v1.36.0...v1.37.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-mbstring
  dependency-version: 1.37.0
  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-04-29 23:11:23 +08:00
dependabot[bot]
97dfc1f12f Bump symfony/polyfill-php81 from 1.36.0 to 1.37.0 (#451)
Bumps [symfony/polyfill-php81](https://github.com/symfony/polyfill-php81) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/symfony/polyfill-php81/releases)
- [Commits](https://github.com/symfony/polyfill-php81/compare/v1.36.0...v1.37.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-php81
  dependency-version: 1.37.0
  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-04-29 23:11:11 +08:00
dependabot[bot]
a5ec8a3ff6 Bump symfony/polyfill-intl-idn from 1.36.0 to 1.37.0 (#452)
Bumps [symfony/polyfill-intl-idn](https://github.com/symfony/polyfill-intl-idn) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/symfony/polyfill-intl-idn/releases)
- [Commits](https://github.com/symfony/polyfill-intl-idn/compare/v1.36.0...v1.37.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-intl-idn
  dependency-version: 1.37.0
  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-04-29 23:11:02 +08:00
dependabot[bot]
12bdb6cb67 Bump symfony/polyfill-php82 from 1.36.0 to 1.37.0 (#453)
Bumps [symfony/polyfill-php82](https://github.com/symfony/polyfill-php82) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/symfony/polyfill-php82/releases)
- [Commits](https://github.com/symfony/polyfill-php82/compare/v1.36.0...v1.37.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-php82
  dependency-version: 1.37.0
  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-04-29 23:10:43 +08:00
wmwlwmwl
a99e3b8642 1.修复已有解析记录:修改清空搜索,切换域名没清空搜索,还有显示问题 2.Cloudflare自定义主机名添加CF优选解析和批量CF优选解析 (#456)
* Add files via upload

1.修复已有解析记录:修改清空搜索,切换域名没清空搜索,还有显示问题
2.Cloudflare自定义主机名添加CF优选解析和批量CF优选解析

* Add files via upload
2026-04-29 23:10:31 +08:00
net909
a1cfd470d9 修复批量修改关联证书 2026-04-28 21:18:10 +08:00
wmwlwmwl
945d91386c 1.修复智能解析页面的搜索。2.修复Cloudflare自定义主机名的分页,去掉多余代码 (#447)
* Update RewriteRule in .htaccess for cleaner routing

修复Apache环境下路由重写规则
废弃旧版 index.php/$1 写法,改用兼容新版PHP的PATH_INFO传参方式
解决访问时报错 No input file specified. 问题

* Add files via upload

1.添加DCV 委派一键添加CNAME
2.添加证书验证方法和最低 TLS 版本
3.添加批量添加 修改 删除
4.修复华为云一键txt解析失败(我没其他dns, 其他的需关注)
5.Cloudflare增强改Cloudflare自定义主机名

* 1.添加快速解析 2.Cloudflare自定义主机名添加搜索功能

* Add files via upload

1.Cloudflare自定义主机名自动获取默认线路(支持所有dns,华为云退回之前)
2.优化手机上显示问题
3.一键添加 DCV 委派支持选择要写入的解析域名

* 优化手机显示

* 添加1. 批量 DCV 委派 2. 批量主机名 TXT 验证 3. 批量证书 TXT 验证 4. 批量刷新验证

1. 批量 DCV 委派
2. 批量主机名 TXT 验证
3. 批量证书 TXT 验证
4. 批量刷新验证

* 快速解析改名智能解析,添加已有解析记录和智能批量添加

* 快速解析改名智能解析,添加已有解析记录和智能批量添加

* 由于之前复制保存的,代码有些差异

* 修复已有解析记录的备注功能

* 备注按dns显示

* 修复记录值过长无法复制,优化显示

* 优化显示

* 1.修复智能解析页面的搜索。2.修复Cloudflare自定义主机名的分页,去掉多余代码
2026-04-24 20:03:43 +08:00
wmwlwmwl
668e2b4ceb Cloudflare增强添加DCV 委派+优化,添加快速解析功能,已有解析记录和智能批量添加 (#442)
* Update RewriteRule in .htaccess for cleaner routing

修复Apache环境下路由重写规则
废弃旧版 index.php/$1 写法,改用兼容新版PHP的PATH_INFO传参方式
解决访问时报错 No input file specified. 问题

* Add files via upload

1.添加DCV 委派一键添加CNAME
2.添加证书验证方法和最低 TLS 版本
3.添加批量添加 修改 删除
4.修复华为云一键txt解析失败(我没其他dns, 其他的需关注)
5.Cloudflare增强改Cloudflare自定义主机名

* 1.添加快速解析 2.Cloudflare自定义主机名添加搜索功能

* Add files via upload

1.Cloudflare自定义主机名自动获取默认线路(支持所有dns,华为云退回之前)
2.优化手机上显示问题
3.一键添加 DCV 委派支持选择要写入的解析域名

* 优化手机显示

* 添加1. 批量 DCV 委派 2. 批量主机名 TXT 验证 3. 批量证书 TXT 验证 4. 批量刷新验证

1. 批量 DCV 委派
2. 批量主机名 TXT 验证
3. 批量证书 TXT 验证
4. 批量刷新验证

* 快速解析改名智能解析,添加已有解析记录和智能批量添加

* 快速解析改名智能解析,添加已有解析记录和智能批量添加

* 由于之前复制保存的,代码有些差异

* 修复已有解析记录的备注功能

* 备注按dns显示

* 修复记录值过长无法复制,优化显示

* 优化显示
2026-04-23 23:15:28 +08:00
net909
75a8aa97b8 合并增加 Technitium DNS 支持 2026-04-23 23:12:32 +08:00
dependabot[bot]
29bcd293ef Bump symfony/polyfill-php82 from 1.35.0 to 1.36.0 (#436)
Bumps [symfony/polyfill-php82](https://github.com/symfony/polyfill-php82) from 1.35.0 to 1.36.0.
- [Release notes](https://github.com/symfony/polyfill-php82/releases)
- [Commits](https://github.com/symfony/polyfill-php82/compare/v1.35.0...v1.36.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-php82
  dependency-version: 1.36.0
  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-04-23 22:52:04 +08:00
dependabot[bot]
b267d3df86 Bump symfony/polyfill-mbstring from 1.35.0 to 1.36.0 (#437)
Bumps [symfony/polyfill-mbstring](https://github.com/symfony/polyfill-mbstring) from 1.35.0 to 1.36.0.
- [Release notes](https://github.com/symfony/polyfill-mbstring/releases)
- [Commits](https://github.com/symfony/polyfill-mbstring/compare/v1.35.0...v1.36.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-mbstring
  dependency-version: 1.36.0
  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-04-23 22:51:09 +08:00
dependabot[bot]
50edcd6dac Bump symfony/polyfill-intl-idn from 1.35.0 to 1.36.0 (#438)
Bumps [symfony/polyfill-intl-idn](https://github.com/symfony/polyfill-intl-idn) from 1.35.0 to 1.36.0.
- [Release notes](https://github.com/symfony/polyfill-intl-idn/releases)
- [Commits](https://github.com/symfony/polyfill-intl-idn/compare/v1.35.0...v1.36.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-intl-idn
  dependency-version: 1.36.0
  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-04-23 22:50:58 +08:00
dependabot[bot]
04980fcdd3 Bump symfony/polyfill-php81 from 1.35.0 to 1.36.0 (#439)
Bumps [symfony/polyfill-php81](https://github.com/symfony/polyfill-php81) from 1.35.0 to 1.36.0.
- [Release notes](https://github.com/symfony/polyfill-php81/releases)
- [Commits](https://github.com/symfony/polyfill-php81/compare/v1.35.0...v1.36.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-php81
  dependency-version: 1.36.0
  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-04-23 22:50:45 +08:00
TomyJan
07a0f54bc1 feat: 新增 Nginx Proxy Manager 证书部署适配 (#446) 2026-04-23 22:48:55 +08:00
小玖
db418c7a11 更新同系统对接插件 (#445)
域名账户新增页可选择同系统对接,需传入对方系统网址;用户ID;API密钥

Co-authored-by: 小玖 <232709122+xiaojiu-code@users.noreply.github.com>
2026-04-22 22:49:32 +08:00
dependabot[bot]
8e4848c14c Bump symfony/polyfill-php81 from 1.34.0 to 1.35.0 (#431)
Bumps [symfony/polyfill-php81](https://github.com/symfony/polyfill-php81) from 1.34.0 to 1.35.0.
- [Release notes](https://github.com/symfony/polyfill-php81/releases)
- [Commits](https://github.com/symfony/polyfill-php81/compare/v1.34.0...v1.35.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-php81
  dependency-version: 1.35.0
  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-04-15 18:22:17 +08:00
dependabot[bot]
8cbc1f9a18 Bump symfony/polyfill-mbstring from 1.34.0 to 1.35.0 (#432)
Bumps [symfony/polyfill-mbstring](https://github.com/symfony/polyfill-mbstring) from 1.34.0 to 1.35.0.
- [Release notes](https://github.com/symfony/polyfill-mbstring/releases)
- [Commits](https://github.com/symfony/polyfill-mbstring/compare/v1.34.0...v1.35.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-mbstring
  dependency-version: 1.35.0
  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-04-15 18:22:09 +08:00
dependabot[bot]
ccda489e81 Bump symfony/polyfill-php82 from 1.34.0 to 1.35.0 (#433)
Bumps [symfony/polyfill-php82](https://github.com/symfony/polyfill-php82) from 1.34.0 to 1.35.0.
- [Release notes](https://github.com/symfony/polyfill-php82/releases)
- [Commits](https://github.com/symfony/polyfill-php82/compare/v1.34.0...v1.35.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-php82
  dependency-version: 1.35.0
  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-04-15 18:21:59 +08:00
dependabot[bot]
45af1ad464 Bump symfony/polyfill-intl-idn from 1.34.0 to 1.35.0 (#434)
Bumps [symfony/polyfill-intl-idn](https://github.com/symfony/polyfill-intl-idn) from 1.34.0 to 1.35.0.
- [Release notes](https://github.com/symfony/polyfill-intl-idn/releases)
- [Commits](https://github.com/symfony/polyfill-intl-idn/compare/v1.34.0...v1.35.0)

---
updated-dependencies:
- dependency-name: symfony/polyfill-intl-idn
  dependency-version: 1.35.0
  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-04-15 18:21:48 +08:00
net909
7e49a40057 update workflows 2026-04-13 22:47:12 +08:00
21 changed files with 6866 additions and 137 deletions

View File

@@ -52,13 +52,16 @@ COPY config/php.ini ${PHP_INI_DIR}/conf.d/custom.ini
# Configure supervisord
COPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# CACHE_BUST 须写进每条相关 RUN否则 GHA/BuildKit 可能单独命中 composer 相关层缓存vendor 仍来自旧构建
ARG CACHE_BUST=local
# Add application
RUN mkdir -p /usr/src && wget --no-cache https://github.com/netcccyun/dnsmgr/archive/refs/heads/main.zip -O /usr/src/www.zip && unzip /usr/src/www.zip -d /usr/src/ && mv /usr/src/dnsmgr-main /usr/src/www && rm -f /usr/src/www.zip
RUN mkdir -p /usr/src && echo "$CACHE_BUST" >/dev/null && wget --no-cache https://github.com/netcccyun/dnsmgr/archive/refs/heads/main.zip -O /usr/src/www.zip && unzip /usr/src/www.zip -d /usr/src/ && mv /usr/src/dnsmgr-main /usr/src/www && rm -f /usr/src/www.zip
# Install composer
RUN wget https://getcomposer.org/download/latest-stable/composer.phar -O /usr/local/bin/composer && chmod +x /usr/local/bin/composer
# Install composer(与下面 install 一并随 CACHE_BUST 失效)
RUN echo "$CACHE_BUST" >/dev/null && wget https://getcomposer.org/download/latest-stable/composer.phar -O /usr/local/bin/composer && chmod +x /usr/local/bin/composer
RUN composer install -d /usr/src/www --no-interaction --no-dev --optimize-autoloader
RUN echo "$CACHE_BUST" >/dev/null && composer install -d /usr/src/www --no-interaction --no-dev --optimize-autoloader --no-cache
RUN adduser -D -s /sbin/nologin -g www www && chown -R www.www /usr/src/www /var/lib/nginx /var/log/nginx

View File

@@ -46,6 +46,9 @@ jobs:
file: .github/docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
# 每次运行唯一,打破「下载源码 + composer」等层的缓存否则会一直用首次构建时的层
build-args: |
CACHE_BUST=${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
# 避免向仓库推送 attestations部分镜像仓库含部分 SWR 场景)无法解析导致 “fail to parse manifest.json”
provenance: false
sbom: false

View File

@@ -751,7 +751,7 @@ class Cert extends BaseController
$ids = input('post.ids');
$success = 0;
$certid = 0;
if (input('post.action') == 'cert') {
if (input('post.act') == 'cert') {
$certid = input('post.certid/d');
$cert = Db::name('cert_order')->where('id', $certid)->find();
if (!$cert) return json(['code' => -1, 'msg' => '证书订单不存在']);

View File

@@ -43,15 +43,23 @@ class Cloudflare extends BaseController
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostname = trim(input('post.hostname', '', 'trim'));
$origin = trim(input('post.custom_origin_server', '', 'trim'));
$sslMethod = trim(input('post.ssl_method', 'txt', 'trim'));
$minTlsVersion = trim(input('post.min_tls_version', '1.0', 'trim'));
if (empty($hostname) || !checkDomain($hostname)) {
throw new Exception('主机名格式不正确');
}
if (!in_array($sslMethod, ['txt', 'http'])) {
throw new Exception('证书验证方法无效');
}
if (!in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
throw new Exception('最低 TLS 版本无效');
}
if ($origin !== '') {
$this->validateCustomOrigin($origin);
}
$result = $context['service']->createCustomHostname($context['domain']['thirdid'], $hostname, $origin !== '' ? $origin : null);
$this->add_log($context['domain']['name'], '创建自定义主机名', $hostname . ($origin !== '' ? ' -> ' . $origin : ''));
$result = $context['service']->createCustomHostname($context['domain']['thirdid'], $hostname, $origin !== '' ? $origin : null, $sslMethod, $minTlsVersion);
$this->add_log($context['domain']['name'], '创建自定义主机名', $hostname . ($origin !== '' ? ' -> ' . $origin : '') . ' (验证: ' . $sslMethod . ', TLS: ' . $minTlsVersion . ')');
return json(['code' => 0, 'msg' => '创建自定义主机名成功', 'data' => $this->formatCustomHostnameRow($result)]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
@@ -70,6 +78,14 @@ class Cloudflare extends BaseController
$current = $context['service']->getCustomHostname($context['domain']['thirdid'], $hostnameId);
$hostname = trim((string)($current['hostname'] ?? ''));
$origin = trim(input('post.custom_origin_server', '', 'trim'));
$sslMethod = trim(input('post.ssl_method', 'txt', 'trim'));
$minTlsVersion = trim(input('post.min_tls_version', '1.0', 'trim'));
if (!in_array($sslMethod, ['txt', 'http'])) {
throw new Exception('证书验证方法无效');
}
if (!in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
throw new Exception('最低 TLS 版本无效');
}
if ($origin !== '') {
$this->validateCustomOrigin($origin);
}
@@ -79,10 +95,10 @@ class Cloudflare extends BaseController
$hostnameId,
[
'custom_origin_server' => $origin !== '' ? $origin : null,
'ssl' => $this->extractCustomHostnameSslPayload($current),
'ssl' => $this->extractCustomHostnameSslPayload($current, $sslMethod, $minTlsVersion),
]
);
$this->add_log($context['domain']['name'], '编辑自定义主机名', $hostname . ' -> ' . ($origin !== '' ? $origin : '清空源站'));
$this->add_log($context['domain']['name'], '编辑自定义主机名', $hostname . ' -> ' . ($origin !== '' ? $origin : '清空源站') . ' (验证: ' . $sslMethod . ', TLS: ' . $minTlsVersion . ')');
return json(['code' => 0, 'msg' => '更新自定义主机名成功', 'data' => $this->formatCustomHostnameRow($result)]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
@@ -109,8 +125,8 @@ class Cloudflare extends BaseController
'ssl' => $this->extractCustomHostnameSslPayload($current),
]
);
$this->add_log($context['domain']['name'], '刷新自定义主机名验', $hostname);
return json(['code' => 0, 'msg' => '已向 Cloudflare 重新发起验', 'data' => $this->formatCustomHostnameRow($result)]);
$this->add_log($context['domain']['name'], '刷新自定义主机名验', $hostname);
return json(['code' => 0, 'msg' => '已向 Cloudflare 重新发起验', 'data' => $this->formatCustomHostnameRow($result)]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
@@ -125,15 +141,170 @@ class Cloudflare extends BaseController
if ($hostnameId === '') {
throw new Exception('缺少 hostname_id');
}
$context['service']->deleteCustomHostname($context['domain']['thirdid'], $hostnameId);
$this->add_log($context['domain']['name'], '删除自定义主机名', $hostname !== '' ? $hostname : $hostnameId);
$this->add_log($context['domain']['name'], '删除自定义主机名', $hostname);
return json(['code' => 0, 'msg' => '删除自定义主机名成功']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function hostnames_batch_delete()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostnameIds = input('post.hostname_ids/a', []);
if (empty($hostnameIds)) {
throw new Exception('缺少 hostname_ids');
}
$deletedCount = 0;
foreach ($hostnameIds as $hostnameId) {
if (trim((string)$hostnameId) !== '') {
try {
// 获取主机名信息用于日志
$hostnameInfo = $context['service']->getCustomHostname($context['domain']['thirdid'], trim((string)$hostnameId));
$hostname = trim((string)($hostnameInfo['hostname'] ?? ''));
$context['service']->deleteCustomHostname($context['domain']['thirdid'], trim((string)$hostnameId));
$deletedCount++;
// 为每个成功删除的主机名记录单独的日志
$this->add_log($context['domain']['name'], '批量删除自定义主机名', $hostname);
} catch (Exception $e) {
// 忽略删除失败的情况,继续处理其他主机名
}
}
}
return json(['code' => 0, 'msg' => '批量删除成功,共删除 ' . $deletedCount . ' 个自定义主机名']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function hostnames_batch_update()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostnameIds = input('post.hostname_ids/s', '');
$hostnameIdArray = array_filter(array_map('trim', explode(',', $hostnameIds)));
if (empty($hostnameIdArray)) {
throw new Exception('缺少 hostname_ids');
}
$origin = trim(input('post.custom_origin_server', '', 'trim'));
$sslMethod = trim(input('post.ssl_method', '', 'trim'));
$minTlsVersion = trim(input('post.min_tls_version', '', 'trim'));
if (!empty($sslMethod) && !in_array($sslMethod, ['txt', 'http'])) {
throw new Exception('证书验证方法无效');
}
if (!empty($minTlsVersion) && !in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
throw new Exception('最低 TLS 版本无效');
}
if ($origin !== '') {
$this->validateCustomOrigin($origin);
}
$updatedCount = 0;
foreach ($hostnameIdArray as $hostnameId) {
if (trim((string)$hostnameId) !== '') {
try {
$current = $context['service']->getCustomHostname($context['domain']['thirdid'], $hostnameId);
$hostname = trim((string)($current['hostname'] ?? ''));
$payload = [];
// 总是设置 custom_origin_server留空时设置为 null 表示清空
$payload['custom_origin_server'] = $origin !== '' ? $origin : null;
if (!empty($sslMethod) || !empty($minTlsVersion)) {
$payload['ssl'] = $this->extractCustomHostnameSslPayload($current, $sslMethod, $minTlsVersion);
}
if (!empty($payload)) {
$context['service']->updateCustomHostname($context['domain']['thirdid'], $hostnameId, $payload);
$updatedCount++;
// 为每个成功修改的主机名记录单独的日志
$logMessage = $hostname . ' -> ' . ($origin !== '' ? $origin : '清空源站') . ' (验证: ' . ($sslMethod ?: '保持不变') . ', TLS: ' . ($minTlsVersion ?: '保持不变') . ')';
$this->add_log($context['domain']['name'], '批量修改自定义主机名', $logMessage);
}
} catch (Exception $e) {
// 忽略修改失败的情况,继续处理其他主机名
}
}
}
return json(['code' => 0, 'msg' => '批量修改成功,共修改 ' . $updatedCount . ' 个自定义主机名']);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function hostnames_batch_add()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$hostnamesText = trim(input('post.hostnames', '', 'trim'));
$origin = trim(input('post.custom_origin_server', '', 'trim'));
$sslMethod = trim(input('post.ssl_method', 'txt', 'trim'));
$minTlsVersion = trim(input('post.min_tls_version', '1.0', 'trim'));
if (empty($hostnamesText)) {
throw new Exception('缺少主机名列表');
}
if (!in_array($sslMethod, ['txt', 'http'])) {
throw new Exception('证书验证方法无效');
}
if (!in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
throw new Exception('最低 TLS 版本无效');
}
if ($origin !== '') {
$this->validateCustomOrigin($origin);
}
$hostnames = array_filter(array_map('trim', explode("\n", $hostnamesText)));
if (empty($hostnames)) {
throw new Exception('主机名列表为空');
}
$addedCount = 0;
$failedHostnames = [];
foreach ($hostnames as $hostname) {
if (empty($hostname)) {
continue;
}
if (!checkDomain($hostname)) {
$failedHostnames[] = $hostname . '(格式不正确)';
continue;
}
try {
$context['service']->createCustomHostname(
$context['domain']['thirdid'],
$hostname,
$origin !== '' ? $origin : null,
$sslMethod,
$minTlsVersion
);
$addedCount++;
// 为每个成功添加的主机名记录单独的日志
$logMessage = $hostname . ($origin !== '' ? ' -> ' . $origin : '') . ' (验证: ' . $sslMethod . ', TLS: ' . $minTlsVersion . ')';
$this->add_log($context['domain']['name'], '批量添加自定义主机名', $logMessage);
} catch (Exception $e) {
$failedHostnames[] = $hostname . '' . $e->getMessage() . '';
}
}
$message = '批量添加成功,共添加 ' . $addedCount . ' 个自定义主机名';
if (!empty($failedHostnames)) {
$message .= ',失败 ' . count($failedHostnames) . ' 个:' . implode('; ', $failedHostnames);
}
return json(['code' => 0, 'msg' => $message]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function hostnames_txt_targets()
{
try {
@@ -196,6 +367,96 @@ class Cloudflare extends BaseController
}
}
public function dcv_delegation_uuid()
{
try {
$context = $this->getCloudflareDomainContext(input('param.id/d'));
$uuid = $context['service']->getDcvDelegationUuid($context['domain']['thirdid']);
return json(['code' => 0, 'data' => ['uuid' => $uuid]]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function get_domain_default_line()
{
try {
$domainId = input('param.domain_id/d');
if (empty($domainId)) {
throw new Exception('缺少 domain_id 参数');
}
// 查询域名信息
$domainRow = Db::name('domain')->alias('A')
->join('account B', 'A.aid = B.id')
->where('A.id', $domainId)
->field('A.*, B.type, B.config account_config')
->find();
if (!$domainRow) {
throw new Exception('域名不存在');
}
// 获取该域名的默认线路
$recordLine = cache('record_line_' . $domainId);
if (empty($recordLine)) {
// 缓存中没有,需要从 DNS 提供商获取
$config = json_decode($domainRow['account_config'] ?? '', true);
if (!is_array($config)) {
$config = [];
}
$dnsModel = \app\lib\DnsHelper::getModel(
intval($domainRow['aid']),
$domainRow['name'],
$domainRow['thirdid'],
$domainRow['type'],
$config
);
if ($dnsModel && method_exists($dnsModel, 'getRecordLine')) {
$recordLine = $dnsModel->getRecordLine();
if ($recordLine && is_array($recordLine)) {
cache('record_line_' . $domainId, $recordLine, 604800); // 缓存7天
}
}
}
if (empty($recordLine) || !is_array($recordLine)) {
throw new Exception('无法获取该域名的解析线路列表');
}
$firstKey = array_key_first($recordLine);
if ($firstKey === null) {
throw new Exception('解析线路列表为空');
}
$lines = [];
foreach ($recordLine as $lineValue => $lineLabel) {
if (is_array($lineLabel)) {
$lines[] = [
'value' => strval($lineValue),
'label' => isset($lineLabel['name']) ? strval($lineLabel['name']) : strval($lineValue),
'parent' => isset($lineLabel['parent']) ? ($lineLabel['parent'] !== null ? strval($lineLabel['parent']) : '') : '',
'is_default' => ($lineValue === $firstKey)
];
} else {
$lines[] = [
'value' => strval($lineValue),
'label' => strval($lineLabel),
'parent' => '',
'is_default' => ($lineValue === $firstKey)
];
}
}
return json(['code' => 0, 'data' => ['default_line' => strval($firstKey), 'lines' => $lines]]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
public function tunnels()
{
try {
@@ -650,11 +911,11 @@ class Cloudflare extends BaseController
}
}
private function extractCustomHostnameSslPayload(array $row): array
private function extractCustomHostnameSslPayload(array $row, string $sslMethod = '', string $minTlsVersion = ''): array
{
$ssl = isset($row['ssl']) && is_array($row['ssl']) ? $row['ssl'] : [];
$payload = [
'method' => trim((string)($ssl['method'] ?? 'http')),
'method' => $sslMethod !== '' ? $sslMethod : trim((string)($ssl['method'] ?? 'http')),
'type' => trim((string)($ssl['type'] ?? 'dv')),
];
if ($payload['method'] === '') {
@@ -663,6 +924,16 @@ class Cloudflare extends BaseController
if ($payload['type'] === '') {
$payload['type'] = 'dv';
}
// 添加 TLS 版本设置
if ($minTlsVersion !== '') {
$payload['settings'] = [
'min_tls_version' => $minTlsVersion
];
} elseif (isset($ssl['settings']) && is_array($ssl['settings'])) {
$payload['settings'] = $ssl['settings'];
}
return $payload;
}
@@ -755,8 +1026,10 @@ class Cloudflare extends BaseController
'hostname' => trim((string)($row['hostname'] ?? '')),
'custom_origin_server' => trim((string)($row['custom_origin_server'] ?? '')),
'status' => trim((string)($row['status'] ?? '')),
'ssl' => $ssl,
'ssl_status' => trim((string)($ssl['status'] ?? '')),
'ssl_method' => trim((string)($ssl['method'] ?? '')),
'ssl_min_tls_version' => trim((string)($ssl['settings']['min_tls_version'] ?? '')),
'ssl_type' => trim((string)($ssl['type'] ?? '')),
'ssl_validation_status' => $sslValidationStatus,
'verification_status' => $verificationStatus !== '' ? $verificationStatus : '-',

View File

@@ -1005,6 +1005,68 @@ class Domain extends BaseController
return view('log');
}
public function smartparse()
{
if (request()->user['type'] == 'domain') {
return redirect('/record/' . request()->user['id']);
}
$list = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')
->field('A.id, A.name, A.aid, B.type')
->order('A.name', 'asc')
->select();
$domainList = [];
foreach ($list as $row) {
if (request()->user['level'] == 1 && !in_array($row['name'], request()->user['permission'])) {
continue;
}
$dnsTypeName = isset(DnsHelper::$dns_config[$row['type']]) ? DnsHelper::$dns_config[$row['type']]['name'] : $row['type'];
$domainList[] = [
'id' => $row['id'],
'name' => $row['name'],
'dnsType' => $dnsTypeName
];
}
View::assign('domainList', $domainList);
return view();
}
public function quickinfo()
{
$id = input('param.id/d');
$drow = Db::name('domain')->where('id', $id)->find();
if (!$drow) {
return json(['code' => -1, 'msg' => '域名不存在']);
}
if (!checkPermission(0, $drow['name'])) return json(['code' => -1, 'msg' => '无权限']);
try {
list($recordLine, $minTTL) = $this->get_line_and_ttl($drow);
$recordLineArr = [];
foreach ($recordLine as $key => $item) {
$recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']];
}
$dnstype = Db::name('account')->where('id', $drow['aid'])->value('type');
$dnsconfig = DnsHelper::$dns_config[$dnstype];
return json([
'code' => 0,
'data' => [
'recordLine' => $recordLineArr,
'minTTL' => $minTTL ? $minTTL : 1,
'weight' => $dnsconfig['weight'] ?? false,
'remark' => $dnsconfig['remark'] ?? 0
]
]);
} catch (Exception $e) {
return json(['code' => -1, 'msg' => $e->getMessage()]);
}
}
private function add_log($domain, $action, $data)
{
if (strlen($data) > 500) $data = substr($data, 0, 500);

View File

@@ -296,6 +296,59 @@ class DeployHelper
],
'taskinputs' => [],
],
'nginxproxymanager' => [
'name' => 'Nginx Proxy Manager',
'class' => 1,
'icon' => 'npm.svg',
'desc' => '更新 Nginx Proxy Manager 的自定义证书并自动绑定 Proxy Host',
'note' => '填写 Nginx Proxy Manager 面板地址与登录账号密码,系统将通过官方 API 登录并执行证书更新。',
'tasknote' => '如填写证书ID则优先更新该自定义证书留空时系统会根据当前证书订单的域名在 NPM 中匹配 Proxy Host并在首次成功后自动保存证书ID后续续期优先走该ID不再依赖域名匹配。',
'inputs' => [
'url' => [
'name' => '面板地址',
'type' => 'input',
'placeholder' => 'Nginx Proxy Manager 面板地址',
'note' => '填写规则如http://192.168.1.100:81 ,不要带 /api 等后缀',
'required' => true,
],
'email' => [
'name' => '登录邮箱',
'type' => 'input',
'placeholder' => 'NPM 登录邮箱',
'validator' => 'email',
'required' => true,
],
'password' => [
'name' => '登录密码',
'type' => 'input',
'placeholder' => 'NPM 登录密码',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'taskinputs' => [
'id' => [
'name' => '证书ID',
'type' => 'input',
'placeholder' => '留空则按域名匹配 Proxy Host 并自动回填',
'note' => '优先级最高。填写后将直接更新该自定义证书ID仅支持 NPM 中 provider 为 other 的自定义证书。',
],
'host_id' => [
'name' => 'Proxy Host ID',
'type' => 'input',
'placeholder' => '可留空,留空则按域名自动匹配',
'note' => '可选。未填写证书ID时若填写此项则仅处理指定 Proxy Host若留空则按当前证书订单域名自动查找匹配的 Proxy Host。',
],
],
],
'btwaf' => [
'name' => '堡塔云WAF',
'class' => 1,

View File

@@ -518,6 +518,41 @@ class DnsHelper
'page' => true,
'add' => true,
],
'technitium' => [
'name' => 'Technitium',
'icon' => 'technitium.png',
'note' => '',
'config' => [
'url' => [
'name' => 'Server URL',
'type' => 'input',
'placeholder' => 'http://127.0.0.1:5380',
'required' => true,
],
'token' => [
'name' => 'API Token',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 2,
'status' => true,
'redirect' => false,
'log' => false,
'weight' => false,
'page' => true,
'add' => true,
],
'aliyunesa' => [
'name' => '阿里云ESA',
'icon' => 'aliyun.png',
@@ -608,6 +643,47 @@ class DnsHelper
'page' => false,
'add' => false,
],
'dnsmgr' => [
'name' => '同系统对接',
'icon' => 'logo.png',
'note' => '对接其他聚合DNS管理系统站点',
'config' => [
'base_url' => [
'name' => '站点地址',
'type' => 'input',
'placeholder' => '例如https://dns.example.com',
'required' => true,
],
'uid' => [
'name' => '用户 ID',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'key' => [
'name' => 'API 密钥',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 2,
'status' => true,
'redirect' => true,
'log' => false,
'weight' => true,
'page' => false,
'add' => false,
],
];
public static $line_name = [
@@ -627,6 +703,7 @@ class DnsHelper
'spaceship' => ['DEF' => 'default'],
'aliyunesa' => ['DEF' => '0'],
'tencenteo' => ['DEF' => 'Default'],
'cccyun' => ['DEF' => 'default'],
];
public static function getList()

View File

@@ -0,0 +1,375 @@
<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class nginxproxymanager implements DeployInterface
{
private $logger;
private $url;
private $email;
private $password;
private $proxy;
private $token;
public function __construct($config)
{
$this->url = rtrim($config['url'] ?? '', '/');
$this->email = trim($config['email'] ?? '');
$this->password = $config['password'] ?? '';
$this->proxy = isset($config['proxy']) && $config['proxy'] == 1;
}
public function check()
{
if (empty($this->url) || empty($this->email) || empty($this->password)) {
throw new Exception('请填写面板地址、登录邮箱和登录密码');
}
$this->login();
$this->request('GET', '/nginx/certificates');
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$domains = $config['domainList'] ?? [];
$domains = array_values(array_filter(array_map('trim', $domains)));
if (empty($domains)) {
throw new Exception('没有设置要部署的域名');
}
$this->login();
$certificateId = intval($config['id'] ?? 0);
if ($certificateId > 0) {
$this->log('使用配置中的证书ID:' . $certificateId . ' 直接更新 NPM 自定义证书');
$certificate = $this->getCertificate($certificateId);
$this->assertCustomCertificate($certificate, $certificateId);
$this->uploadCertificate($certificateId, $fullchain, $privatekey);
$this->log('证书ID:' . $certificateId . ' 更新成功!');
return;
}
$hostId = intval($config['host_id'] ?? 0);
$hosts = $this->resolveTargetHosts($domains, $hostId);
if (empty($hosts)) {
throw new Exception('未找到匹配的 Proxy Host请填写证书ID或 Proxy Host ID');
}
$this->log('匹配到 Proxy Host ' . count($hosts) . ' 个');
$resolvedCertificateId = 0;
$conflictMessage = null;
foreach ($hosts as $host) {
$hostCertificateId = intval($host['certificate_id'] ?? 0);
if ($hostCertificateId <= 0) {
continue;
}
try {
$certificate = $this->getCertificate($hostCertificateId);
$this->assertCustomCertificate($certificate, $hostCertificateId);
if ($resolvedCertificateId === 0) {
$resolvedCertificateId = $hostCertificateId;
} elseif ($resolvedCertificateId !== $hostCertificateId) {
$conflictMessage = '匹配到多个 Proxy Host但它们绑定了不同的自定义证书ID无法自动决定更新哪个证书请手动填写证书ID';
}
} catch (Exception $e) {
$this->log('Proxy Host ID:' . $host['id'] . ' 当前证书不可直接更新:' . $e->getMessage());
}
}
if ($conflictMessage !== null) {
throw new Exception($conflictMessage);
}
if ($resolvedCertificateId === 0) {
$resolvedCertificateId = $this->createCustomCertificate($domains);
$this->log('创建自定义证书成功证书ID:' . $resolvedCertificateId);
}
$this->uploadCertificate($resolvedCertificateId, $fullchain, $privatekey);
$this->log('证书ID:' . $resolvedCertificateId . ' 更新成功!');
foreach ($hosts as $host) {
$currentCertificateId = intval($host['certificate_id'] ?? 0);
if ($currentCertificateId !== $resolvedCertificateId) {
$this->updateProxyHostCertificate($host, $resolvedCertificateId);
$this->log('Proxy Host ID:' . $host['id'] . ' 已绑定到证书ID:' . $resolvedCertificateId);
} else {
$this->log('Proxy Host ID:' . $host['id'] . ' 已绑定目标证书,无需重复更新绑定');
}
}
$info['config']['id'] = (string)$resolvedCertificateId;
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
private function login()
{
$data = $this->request('POST', '/tokens', [
'identity' => $this->email,
'secret' => $this->password,
], false, false);
if (empty($data['token'])) {
if (!empty($data['requires_2fa'])) {
throw new Exception('当前 NPM 账户启用了双因素认证,暂不支持');
}
throw new Exception('登录 NPM 失败,未返回访问令牌');
}
$this->token = $data['token'];
}
private function resolveTargetHosts(array $domains, int $hostId): array
{
if ($hostId > 0) {
return [$this->getProxyHost($hostId)];
}
$hosts = $this->request('GET', '/nginx/proxy-hosts');
if (!is_array($hosts)) {
throw new Exception('获取 Proxy Host 列表失败');
}
$matched = [];
foreach ($hosts as $host) {
$hostDomains = $host['domain_names'] ?? [];
if ($this->hasIntersectDomain($domains, $hostDomains)) {
$matched[] = $this->getProxyHost(intval($host['id']));
}
}
return $matched;
}
private function hasIntersectDomain(array $domains, array $hostDomains): bool
{
foreach ($hostDomains as $hostDomain) {
$hostDomain = trim((string)$hostDomain);
if ($hostDomain === '') {
continue;
}
foreach ($domains as $domain) {
if ($this->domainMatches($domain, $hostDomain) || $this->domainMatches($hostDomain, $domain)) {
return true;
}
}
}
return false;
}
private function domainMatches(string $pattern, string $domain): bool
{
$pattern = strtolower(trim($pattern));
$domain = strtolower(trim($domain));
if ($pattern === '' || $domain === '') {
return false;
}
if ($pattern === $domain) {
return true;
}
if (str_starts_with($pattern, '*.')) {
$suffix = substr($pattern, 1);
return str_ends_with($domain, $suffix);
}
return false;
}
private function createCustomCertificate(array $domains): int
{
$result = $this->request('POST', '/nginx/certificates', [
'provider' => 'other',
'nice_name' => $this->buildCertificateName($domains),
]);
if (isset($result['owner_user_id'])) {
$this->log('NPM 新建证书归属用户ID:' . intval($result['owner_user_id']) . '(由当前登录账号决定)');
}
$certificateId = intval($result['id'] ?? 0);
if ($certificateId <= 0) {
throw new Exception('创建 NPM 自定义证书失败');
}
return $certificateId;
}
private function buildCertificateName(array $domains): string
{
return trim($domains[0]);
}
private function uploadCertificate(int $certificateId, string $fullchain, string $privatekey): void
{
[$certificate, $intermediateCertificate] = $this->splitFullchain($fullchain);
$multipart = [
[
'name' => 'certificate',
'filename' => 'certificate.pem',
'contents' => $certificate,
],
[
'name' => 'certificate_key',
'filename' => 'certificate.key',
'contents' => $privatekey,
],
];
if ($intermediateCertificate !== '') {
$multipart[] = [
'name' => 'intermediate_certificate',
'filename' => 'intermediate.pem',
'contents' => $intermediateCertificate,
];
}
$this->request(
'POST',
'/nginx/certificates/' . $certificateId . '/upload',
$multipart,
true,
true,
['Content-Type' => 'multipart/form-data']
);
}
private function splitFullchain(string $fullchain): array
{
preg_match_all('/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/s', $fullchain, $matches);
$certificates = array_values(array_filter(array_map('trim', $matches[0] ?? [])));
if (empty($certificates)) {
throw new Exception('证书内容格式错误,未找到 PEM 证书块');
}
$certificate = $certificates[0] . "\n";
$intermediateCertificate = '';
if (count($certificates) > 1) {
$intermediateCertificate = implode("\n", array_slice($certificates, 1)) . "\n";
}
return [$certificate, $intermediateCertificate];
}
private function updateProxyHostCertificate(array $host, int $certificateId): void
{
$payload = [
'certificate_id' => $certificateId,
];
$this->request('PUT', '/nginx/proxy-hosts/' . intval($host['id']), $payload);
}
private function assertCustomCertificate(array $certificate, int $certificateId): void
{
if (($certificate['provider'] ?? '') !== 'other') {
throw new Exception('证书ID:' . $certificateId . ' 不是自定义证书(provider=other),无法通过上传接口更新');
}
}
private function getCertificate(int $certificateId): array
{
$certificate = $this->request('GET', '/nginx/certificates/' . $certificateId);
if (!is_array($certificate) || empty($certificate['id'])) {
throw new Exception('证书ID:' . $certificateId . ' 不存在');
}
return $certificate;
}
private function getProxyHost(int $hostId): array
{
$host = $this->request('GET', '/nginx/proxy-hosts/' . $hostId);
if (!is_array($host) || empty($host['id'])) {
throw new Exception('Proxy Host ID:' . $hostId . ' 不存在');
}
$this->log('读取 Proxy Host ID:' . intval($host['id']) . ' owner_user_id:' . intval($host['owner_user_id'] ?? 0) . ' certificate_id:' . intval($host['certificate_id'] ?? 0));
return $host;
}
private function request(string $method, string $path, $params = null, bool $auth = true, bool $logBodyOnError = true, array $extraHeaders = [])
{
$headers = $extraHeaders;
if (!isset($headers['Content-Type']) && $params !== null && strtoupper($method) !== 'GET') {
$headers['Content-Type'] = 'application/json';
}
if ($auth) {
if (empty($this->token)) {
throw new Exception('NPM 访问令牌不存在,请先登录');
}
$headers['Authorization'] = 'Bearer ' . $this->token;
}
$requestData = $params;
if ($params !== null && isset($headers['Content-Type']) && strtolower($headers['Content-Type']) !== 'multipart/form-data') {
$requestData = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
$response = http_request(
$this->url . '/api' . $path,
$requestData,
null,
null,
$headers,
$this->proxy,
$method,
30
);
$body = $response['body'] ?? '';
$result = json_decode($body, true);
if ($response['code'] >= 200 && $response['code'] < 300) {
return $result;
}
if ($logBodyOnError && $body !== '') {
$this->log('Response:' . $body);
}
if (isset($result['error']['message'])) {
throw new Exception($result['error']['message']);
}
if (isset($result['message'])) {
throw new Exception($result['message']);
}
if (isset($result['error']) && is_string($result['error']) && $result['error'] !== '') {
throw new Exception($result['error']);
}
if ($body !== '') {
throw new Exception('请求失败(httpCode=' . $response['code'] . '): ' . $this->truncateResponseBody($body));
}
throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
}
private function truncateResponseBody(string $body): string
{
$body = trim($body);
if ($body === '') {
return '';
}
if (mb_strlen($body) > 300) {
return mb_substr($body, 0, 300) . '...';
}
return $body;
}
}

282
app/lib/dns/dnsmgr.php Normal file
View File

@@ -0,0 +1,282 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
use Exception;
class dnsmgr implements DnsInterface
{
private $uid;
private $key;
private $baseUrl;
private $error;
private $domain;
private $domainid;
private $proxy;
private $domainInfo;
public function __construct($config)
{
$this->uid = $config['uid'];
$this->key = $config['key'];
$this->baseUrl = rtrim($config['base_url'], '/');
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->proxy = $proxy;
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
}
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 = 20)
{
$offset = ($PageNumber - 1) * $PageSize;
$param = [
'offset' => $offset,
'limit' => $PageSize,
];
if (!isNullOrEmpty($KeyWord)) {
$param['kw'] = $KeyWord;
}
$data = $this->send_request('/api/domain', $param);
if ($data && isset($data['rows'])) {
$list = [];
foreach ($data['rows'] as $row) {
$list[] = [
'DomainId' => $row['id'],
'Domain' => $row['name'],
'RecordCount' => $row['recordcount'],
];
}
return ['total' => $data['total'], 'list' => $list];
}
return false;
}
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$offset = ($PageNumber - 1) * $PageSize;
$param = [
'offset' => $offset,
'limit' => $PageSize,
];
if (!isNullOrEmpty($KeyWord)) $param['keyword'] = $KeyWord;
if (!isNullOrEmpty($SubDomain)) $param['subdomain'] = $SubDomain;
if (!isNullOrEmpty($Value)) $param['value'] = $Value;
if (!isNullOrEmpty($Type)) $param['type'] = $Type;
if (!isNullOrEmpty($Line)) $param['line'] = $Line;
if (!isNullOrEmpty($Status)) $param['status'] = $Status;
$data = $this->send_request('/api/record/data/' . $this->domainid, $param);
if ($data && isset($data['rows'])) {
$list = [];
foreach ($data['rows'] as $row) {
$list[] = [
'RecordId' => $row['RecordId'],
'Domain' => $row['Domain'],
'Name' => $row['Name'],
'Type' => $row['Type'],
'Value' => $row['Value'],
'Line' => $row['Line'],
'LineName' => $row['LineName'],
'TTL' => $row['TTL'],
'MX' => $row['MX'],
'Status' => $row['Status'],
'Weight' => $row['Weight'],
'Remark' => $row['Remark'],
'UpdateTime' => $row['UpdateTime'],
];
}
return ['total' => $data['total'], 'list' => $list];
}
return false;
}
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 getDomainRecordInfo($RecordId)
{
return false;
}
public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$param = [
'name' => $Name,
'type' => $Type,
'value' => $Value,
'line' => $Line,
'ttl' => intval($TTL),
];
if ($Type == 'MX' && !isNullOrEmpty($MX)) {
$param['mx'] = intval($MX);
}
if (!isNullOrEmpty($Weight)) {
$param['weight'] = intval($Weight);
}
if (!isNullOrEmpty($Remark)) {
$param['remark'] = $Remark;
}
$data = $this->send_request('/api/record/add/' . $this->domainid, $param);
return $data !== false;
}
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$param = [
'recordid' => $RecordId,
'name' => $Name,
'type' => $Type,
'value' => $Value,
'line' => $Line,
'ttl' => intval($TTL),
];
if ($Type == 'MX' && !isNullOrEmpty($MX)) {
$param['mx'] = intval($MX);
}
if (!isNullOrEmpty($Weight)) {
$param['weight'] = intval($Weight);
}
if (!isNullOrEmpty($Remark)) {
$param['remark'] = $Remark;
}
$data = $this->send_request('/api/record/update/' . $this->domainid, $param);
return $data !== false;
}
public function updateDomainRecordRemark($RecordId, $Remark)
{
$param = [
'recordid' => $RecordId,
'remark' => $Remark,
];
$data = $this->send_request('/api/record/remark/' . $this->domainid, $param);
return $data !== false;
}
public function deleteDomainRecord($RecordId)
{
$param = [
'recordid' => $RecordId,
];
$data = $this->send_request('/api/record/delete/' . $this->domainid, $param);
return $data !== false;
}
public function setDomainRecordStatus($RecordId, $Status)
{
$param = [
'recordid' => $RecordId,
'status' => $Status,
];
$data = $this->send_request('/api/record/status/' . $this->domainid, $param);
return $data !== false;
}
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
{
return false;
}
public function getRecordLine()
{
$data = $this->getDomainInfo();
if ($data && isset($data['recordLine'])) {
$list = [];
foreach ($data['recordLine'] as $row) {
$list[$row['id']] = [
'name' => $row['name'],
'parent' => isset($row['parent']) ? $row['parent'] : null,
];
}
return $list;
}
return false;
}
public function getMinTTL()
{
$data = $this->getDomainInfo();
if ($data && isset($data['minTTL'])) {
return $data['minTTL'];
}
return false;
}
public function getDomainInfo()
{
if (!empty($this->domainInfo)) return $this->domainInfo;
$data = $this->send_request('/api/domain/' . $this->domainid, ['loginurl' => 0]);
if ($data) {
$this->domainInfo = $data;
return $data;
}
return false;
}
public function addDomain($Domain)
{
return false;
}
private function send_request($path, $param = [])
{
try {
$timestamp = (string)time();
$signStr = $this->uid . $timestamp . $this->key;
$sign = md5($signStr);
$url = $this->baseUrl . $path;
$param['uid'] = $this->uid;
$param['timestamp'] = $timestamp;
$param['sign'] = $sign;
$postData = http_build_query($param);
$response = http_request($url, $postData, null, null, null, $this->proxy);
$result = json_decode($response['body'], true);
if (isset($result['code']) && $result['code'] == 0) {
return isset($result['data']) ? $result['data'] : null;
} elseif (isset($result['rows']) && isset($result['total'])) {
return $result;
} elseif (isset($result['msg'])) {
$this->setError($result['msg']);
return false;
} else {
$this->setError($response['body']);
return false;
}
} catch (Exception $e) {
$this->setError($e->getMessage());
return false;
}
}
private function setError($message)
{
$this->error = $message;
}
}

View File

@@ -63,7 +63,7 @@ class huawei implements DnsInterface
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$offset = ($PageNumber - 1) * $PageSize;
$query = ['type' => $Type, 'line_id' => $Line, 'name' => $KeyWord, 'offset' => $offset, 'limit' => $PageSize];
$query = ['type' => $Type, 'line_id' => $Line, 'name' => $KeyWord, 'offset' => $offset, 'limit' => $PageSize, 'records' => $Value];
if (!isNullOrEmpty($Status)) {
$Status = $Status == '1' ? 'ACTIVE' : 'DISABLE';
$query['status'] = $Status;

499
app/lib/dns/technitium.php Normal file
View File

@@ -0,0 +1,499 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
use Exception;
class technitium implements DnsInterface
{
private $url;
private $token;
private $error;
private $domain;
private $domainid;
private $proxy;
function __construct($config)
{
$this->url = rtrim($config['url'], '/') . '/api';
$this->token = $config['token'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
}
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 = 20)
{
$data = $this->send_request('GET', '/zones/list');
if ($data && isset($data['response']['zones'])) {
$list = [];
foreach ($data['response']['zones'] as $zone) {
$list[] = [
'DomainId' => $zone['name'],
'Domain' => $zone['name'],
'RecordCount' => 0,
];
}
if (!isNullOrEmpty($KeyWord)) {
$list = array_values(array_filter($list, function ($v) use ($KeyWord) {
return strpos($v['Domain'], $KeyWord) !== false;
}));
}
return ['total' => count($list), 'list' => $list];
}
return false;
}
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$params = ['domain' => $this->domain, 'listZone' => 'true'];
$data = $this->send_request('GET', '/zones/records/get', $params);
if ($data && isset($data['response']['records'])) {
$list = [];
$records = $data['response']['records'];
foreach ($records as $i => &$row) {
$row['id'] = $i;
$name = $row['name'] == $this->domain ? '@' : str_replace('.' . $this->domain, '', $row['name']);
$value = '';
$mx = null;
$rData = $row['rData'];
if ($row['type'] == 'A' || $row['type'] == 'AAAA') {
$value = isset($rData['ipAddress']) ? $rData['ipAddress'] : '';
} elseif ($row['type'] == 'CNAME') {
$value = isset($rData['cname']) ? $rData['cname'] : '';
} elseif ($row['type'] == 'NS') {
$value = isset($rData['nameServer']) ? $rData['nameServer'] : '';
} elseif ($row['type'] == 'MX') {
$value = isset($rData['exchange']) ? $rData['exchange'] : '';
$mx = isset($rData['preference']) ? $rData['preference'] : 1;
} elseif ($row['type'] == 'TXT') {
$value = isset($rData['text']) ? $rData['text'] : '';
} elseif ($row['type'] == 'SRV') {
$value = (isset($rData['priority']) ? $rData['priority'] : 0) . ' ' . (isset($rData['weight']) ? $rData['weight'] : 0) . ' ' . (isset($rData['port']) ? $rData['port'] : 0) . ' ' . (isset($rData['target']) ? $rData['target'] : '');
} elseif ($row['type'] == 'PTR') {
$value = isset($rData['ptrName']) ? $rData['ptrName'] : '';
} elseif ($row['type'] == 'CAA') {
$value = (isset($rData['flags']) ? $rData['flags'] : 0) . ' ' . (isset($rData['tag']) ? $rData['tag'] : '') . ' "' . (isset($rData['value']) ? $rData['value'] : '') . '"';
} elseif ($row['type'] == 'ANAME') {
$value = isset($rData['aname']) ? $rData['aname'] : '';
} elseif ($row['type'] == 'DNAME') {
$value = isset($rData['dname']) ? $rData['dname'] : '';
} elseif ($row['type'] == 'APP') {
$value = (isset($rData['appName']) ? $rData['appName'] : '') . ' ' . (isset($rData['classPath']) ? $rData['classPath'] : '');
if (!empty($rData['recordData'])) {
$value .= ' ' . $rData['recordData'];
}
}
$list[] = [
'RecordId' => $i,
'Domain' => $this->domain,
'Name' => $name,
'Type' => $row['type'],
'Value' => $value,
'Line' => 'default',
'TTL' => $row['ttl'],
'MX' => $mx,
'Status' => $row['disabled'] ? '0' : '1',
'Weight' => null,
'Remark' => isset($row['comments']) ? $row['comments'] : null,
'UpdateTime' => null,
];
}
cache('technitium_' . $this->domain, $records, 86400);
if (!isNullOrEmpty($SubDomain)) {
$list = array_values(array_filter($list, function ($v) use ($SubDomain) {
return strcasecmp($v['Name'], $SubDomain) === 0;
}));
} else {
if (!isNullOrEmpty($KeyWord)) {
$list = array_values(array_filter($list, function ($v) use ($KeyWord) {
return strpos($v['Name'], $KeyWord) !== false || strpos($v['Value'], $KeyWord) !== false;
}));
}
if (!isNullOrEmpty($Value)) {
$list = array_values(array_filter($list, function ($v) use ($Value) {
return $v['Value'] == $Value;
}));
}
if (!isNullOrEmpty($Type)) {
$list = array_values(array_filter($list, function ($v) use ($Type) {
return $v['Type'] == $Type;
}));
}
if (!isNullOrEmpty($Status)) {
$list = array_values(array_filter($list, function ($v) use ($Status) {
return $v['Status'] == $Status;
}));
}
}
return ['total' => count($list), 'list' => $list];
}
return false;
}
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
{
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
}
public function getDomainRecordInfo($RecordId)
{
return false;
}
private function buildRecordParams($Type, $Value, $MX = 1)
{
$params = [];
if ($Type == 'A' || $Type == 'AAAA') {
$params['ipAddress'] = $Value;
} elseif ($Type == 'CNAME') {
$params['cname'] = $Value;
} elseif ($Type == 'NS') {
$params['nameServer'] = $Value;
} elseif ($Type == 'MX') {
$params['exchange'] = $Value;
$params['preference'] = intval($MX);
} elseif ($Type == 'TXT') {
$params['text'] = $Value;
} elseif ($Type == 'SRV') {
$parts = explode(' ', $Value);
if (count($parts) == 4) {
$params['priority'] = $parts[0];
$params['weight'] = $parts[1];
$params['port'] = $parts[2];
$params['target'] = $parts[3];
}
} elseif ($Type == 'PTR') {
$params['ptrName'] = $Value;
} elseif ($Type == 'CAA') {
$parts = explode(' ', $Value, 3);
if (count($parts) == 3) {
$params['flags'] = $parts[0];
$params['tag'] = $parts[1];
$params['value'] = trim($parts[2], '"');
}
} elseif ($Type == 'ANAME') {
$params['aname'] = $Value;
} elseif ($Type == 'DNAME') {
$params['dname'] = $Value;
} elseif ($Type == 'APP') {
$parts = explode(' ', $Value, 3);
if (count($parts) >= 2) {
$params['appName'] = $parts[0];
$params['classPath'] = $parts[1];
$params['recordData'] = rtrim(isset($parts[2]) ? $parts[2] : '');
} else {
$params['appName'] = rtrim($Value);
}
}
return $params;
}
private function getOldValueParams($Type, $rData)
{
$params = [];
if ($Type == 'A' || $Type == 'AAAA') {
$params['ipAddress'] = isset($rData['ipAddress']) ? $rData['ipAddress'] : '';
} elseif ($Type == 'CNAME') {
$params['cname'] = isset($rData['cname']) ? $rData['cname'] : '';
} elseif ($Type == 'NS') {
$params['nameServer'] = isset($rData['nameServer']) ? $rData['nameServer'] : '';
} elseif ($Type == 'MX') {
$params['exchange'] = isset($rData['exchange']) ? $rData['exchange'] : '';
$params['preference'] = isset($rData['preference']) ? $rData['preference'] : 1;
} elseif ($Type == 'TXT') {
$params['text'] = isset($rData['text']) ? $rData['text'] : '';
} elseif ($Type == 'SRV') {
$params['priority'] = isset($rData['priority']) ? $rData['priority'] : 0;
$params['weight'] = isset($rData['weight']) ? $rData['weight'] : 0;
$params['port'] = isset($rData['port']) ? $rData['port'] : 0;
$params['target'] = isset($rData['target']) ? $rData['target'] : '';
} elseif ($Type == 'PTR') {
$params['ptrName'] = isset($rData['ptrName']) ? $rData['ptrName'] : '';
} elseif ($Type == 'CAA') {
$params['flags'] = isset($rData['flags']) ? $rData['flags'] : 0;
$params['tag'] = isset($rData['tag']) ? $rData['tag'] : '';
$params['value'] = isset($rData['value']) ? $rData['value'] : '';
} elseif ($Type == 'ANAME') {
$params['aname'] = isset($rData['aname']) ? $rData['aname'] : '';
} elseif ($Type == 'DNAME') {
$params['dname'] = isset($rData['dname']) ? $rData['dname'] : '';
} elseif ($Type == 'APP') {
$params['appName'] = isset($rData['appName']) ? $rData['appName'] : '';
$params['classPath'] = isset($rData['classPath']) ? $rData['classPath'] : '';
if (!empty($rData['recordData'])) {
$params['recordData'] = $rData['recordData'];
}
}
return $params;
}
private function getNewValueParams($Type, $Value, $MX = 1)
{
$params = [];
if ($Type == 'A' || $Type == 'AAAA') {
$params['newIpAddress'] = $Value;
} elseif ($Type == 'CNAME') {
$params['newCname'] = $Value;
} elseif ($Type == 'NS') {
$params['newNameServer'] = $Value;
} elseif ($Type == 'MX') {
$params['newExchange'] = $Value;
$params['newPreference'] = intval($MX);
} elseif ($Type == 'TXT') {
$params['newText'] = $Value;
} elseif ($Type == 'SRV') {
$parts = explode(' ', $Value);
if (count($parts) == 4) {
$params['newPriority'] = $parts[0];
$params['newWeight'] = $parts[1];
$params['newPort'] = $parts[2];
$params['newTarget'] = $parts[3];
}
} elseif ($Type == 'PTR') {
$params['newPtrName'] = $Value;
} elseif ($Type == 'CAA') {
$parts = explode(' ', $Value, 3);
if (count($parts) == 3) {
$params['newFlags'] = $parts[0];
$params['newTag'] = $parts[1];
$params['newValue'] = trim($parts[2], '"');
}
} elseif ($Type == 'ANAME') {
$params['newAName'] = $Value;
} elseif ($Type == 'DNAME') {
$params['newDName'] = $Value;
} elseif ($Type == 'APP') {
$parts = explode(' ', $Value, 3);
if (count($parts) >= 2) {
$params['appName'] = $parts[0];
$params['classPath'] = $parts[1];
$params['recordData'] = rtrim(isset($parts[2]) ? $parts[2] : '');
} else {
$params['appName'] = rtrim($Value);
}
}
return $params;
}
public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$domain = $Name == '@' ? $this->domain : $Name . '.' . $this->domain;
$params = [
'domain' => $domain,
'zone' => $this->domain,
'type' => $Type,
'ttl' => intval($TTL)
];
if (!isNullOrEmpty($Remark)) {
$params['comments'] = $Remark;
}
$valParams = $this->buildRecordParams($Type, $Value, $MX);
if (empty($valParams) && $Type != 'SOA') {
$this->setError('不受支持的记录类型或参数解析失败');
return false;
}
$params = array_merge($params, $valParams);
$result = $this->send_request('POST', '/zones/records/add', $params);
return $result !== false;
}
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$records = cache('technitium_' . $this->domain);
if (!$records || !isset($records[$RecordId])) {
$this->setError('记录不存在,请刷新页面重试');
return false;
}
$oldRecord = $records[$RecordId];
$domain = $oldRecord['name'];
$newDomain = $Name == '@' ? $this->domain : $Name . '.' . $this->domain;
if ($oldRecord['type'] == 'APP') {
$oldValue = (isset($oldRecord['rData']['appName']) ? $oldRecord['rData']['appName'] : '') . ' ' . (isset($oldRecord['rData']['classPath']) ? $oldRecord['rData']['classPath'] : '');
if (!empty($oldRecord['rData']['recordData'])) {
$oldValue .= ' ' . $oldRecord['rData']['recordData'];
}
if ($oldValue != rtrim($Value) || $domain != $newDomain) {
$this->deleteDomainRecord($RecordId);
return $this->addDomainRecord($Name, $Type, $Value, $Line, $TTL, $MX, $Weight, $Remark);
}
}
$params = [
'domain' => $domain,
'zone' => $this->domain,
'type' => $oldRecord['type'],
'ttl' => intval($TTL),
];
if ($domain != $newDomain) {
$params['newDomain'] = $newDomain;
}
$params['comments'] = empty($Remark) ? "" : $Remark;
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
$newValParams = $this->getNewValueParams($Type, $Value, $MX);
$params = array_merge($params, $oldValParams, $newValParams);
$result = $this->send_request('POST', '/zones/records/update', $params);
return $result !== false;
}
public function updateDomainRecordRemark($RecordId, $Remark)
{
$records = cache('technitium_' . $this->domain);
if (!$records || !isset($records[$RecordId])) {
$this->setError('记录不存在,请刷新页面重试');
return false;
}
$oldRecord = $records[$RecordId];
$domain = $oldRecord['name'];
$params = [
'domain' => $domain,
'zone' => $this->domain,
'type' => $oldRecord['type'],
'comments' => $Remark,
];
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
$params = array_merge($params, $oldValParams);
$result = $this->send_request('POST', '/zones/records/update', $params);
return $result !== false;
}
public function deleteDomainRecord($RecordId)
{
$records = cache('technitium_' . $this->domain);
if (!$records || !isset($records[$RecordId])) {
$this->setError('记录不存在,请刷新页面重试');
return false;
}
$oldRecord = $records[$RecordId];
$domain = $oldRecord['name'];
$params = [
'domain' => $domain,
'zone' => $this->domain,
'type' => $oldRecord['type'],
];
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
$params = array_merge($params, $oldValParams);
$result = $this->send_request('POST', '/zones/records/delete', $params);
return $result !== false;
}
public function setDomainRecordStatus($RecordId, $Status)
{
$records = cache('technitium_' . $this->domain);
if (!$records || !isset($records[$RecordId])) {
$this->setError('记录不存在,请刷新页面重试');
return false;
}
$oldRecord = $records[$RecordId];
$domain = $oldRecord['name'];
$params = [
'domain' => $domain,
'zone' => $this->domain,
'type' => $oldRecord['type'],
'disable' => $Status == '0' ? 'true' : 'false',
];
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
$params = array_merge($params, $oldValParams);
$result = $this->send_request('POST', '/zones/records/update', $params);
return $result !== false;
}
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 getMinTTL()
{
return false;
}
public function addDomain($Domain)
{
$params = [
'zone' => $Domain,
'type' => 'Primary'
];
$result = $this->send_request('POST', '/zones/create', $params);
if ($result && isset($result['response']['domain'])) {
return ['id' => $result['response']['domain'], 'name' => $result['response']['domain']];
}
return false;
}
private function send_request($method, $path, $params = [])
{
$url = $this->url . $path;
$params['token'] = $this->token;
$body = null;
if ($method == 'GET' || $method == 'DELETE') {
$url .= '?' . http_build_query($params);
} else {
$body = http_build_query($params);
}
try {
$response = http_request($url, $body, null, null, null, $this->proxy, $method);
} catch (Exception $e) {
$this->setError($e->getMessage());
return false;
}
$arr = json_decode($response['body'], true);
if (isset($arr['status']) && $arr['status'] == 'ok') {
return $arr;
} elseif (isset($arr['errorMessage'])) {
$this->setError($arr['errorMessage']);
return false;
} else {
$this->setError('API 请求失败');
return false;
}
}
private function setError($message)
{
$this->error = $message;
}
}

View File

@@ -91,14 +91,17 @@ class CloudflareEnhanceService
}
}
public function createCustomHostname(string $zoneId, string $hostname, ?string $customOriginServer = null): array
public function createCustomHostname(string $zoneId, string $hostname, ?string $customOriginServer = null, string $sslMethod = 'http', string $minTlsVersion = '1.0'): array
{
$hostname = $this->normalizeHostname($hostname);
$payload = [
'hostname' => $hostname,
'ssl' => [
'method' => 'http',
'method' => $sslMethod === 'txt' ? 'txt' : 'http',
'type' => 'dv',
'settings' => [
'min_tls_version' => $minTlsVersion
]
],
];
$origin = trim((string)$customOriginServer);
@@ -180,6 +183,19 @@ class CloudflareEnhanceService
}
}
public function getDcvDelegationUuid(string $zoneId): string
{
try {
$result = $this->requestResult('GET', '/zones/' . $zoneId . '/dcv_delegation/uuid', [], null, true);
if ($result === null) {
return '';
}
return trim((string)($result['uuid'] ?? ''));
} catch (Exception $e) {
$this->throwActionError('获取 DCV 委派 UUID', $e, 'SSL and Certificates:Read');
}
}
public function listTunnels(string $accountId): array
{
$this->assertTunnelSupported();

File diff suppressed because it is too large Load Diff

View File

@@ -106,9 +106,12 @@
{if request()->user['type'] eq 'user'}<li class="{:checkIfActive('index')}">
<a href="/"><i class="fa fa-home fa-fw"></i> <span>后台首页</span></a>
</li>{/if}
<li class="{:checkIfActive('domain,record,record_log,record_batch_add,domain_add,weight,record_batch_add2,record_batch_edit2,expire_notice')}">
<li class="{:checkIfActive('domain,record,record_log,record_batch_add,domain_add,weight,record_batch_add2,record_batch_edit2,expire_notice,smartparse')}">
<a href="/domain"><i class="fa fa-list-ul fa-fw"></i> <span>域名管理</span></a>
</li>
<li class="{:checkIfActive('smartparse')}">
<a href="/record/smartparse"><i class="fa fa-magic fa-fw"></i> <span>智能解析</span></a>
</li>
{if request()->user['level'] eq 2}
<li class="{:checkIfActive('account,account_add')}">
<a href="/account"><i class="fa fa-lock fa-fw"></i> <span>域名账户</span></a>

View File

@@ -183,7 +183,7 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
<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:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加记录</a>
{if $dnsconfig.type=='cloudflare' && $user['level'] eq 2}<a href="/cloudflare/hostnames/{$domainId}" class="btn btn-default">Cloudflare增强</a>{/if}
{if $dnsconfig.type=='cloudflare' && $user['level'] eq 2}<a href="/cloudflare/hostnames/{$domainId}" class="btn btn-default">Cloudflare自定义主机名</a>{/if}
{if $dnsconfig.type=='aliyun'}<a href="/record/weight/{$domainId}" class="btn btn-default">权重配置</a>{/if}
{if $dnsconfig.type=='dnspod'}<a href="/record/alias/{$domainId}" class="btn btn-default">域名别名</a>{/if}
<div class="btn-group" role="group">

File diff suppressed because it is too large Load Diff

20
composer.lock generated
View File

@@ -1036,7 +1036,7 @@
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
@@ -1099,7 +1099,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0"
},
"funding": [
{
@@ -1123,7 +1123,7 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -1184,7 +1184,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0"
},
"funding": [
{
@@ -1208,7 +1208,7 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
@@ -1269,7 +1269,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0"
},
"funding": [
{
@@ -1293,7 +1293,7 @@
},
{
"name": "symfony/polyfill-php81",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
@@ -1349,7 +1349,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0"
},
"funding": [
{
@@ -1373,7 +1373,7 @@
},
{
"name": "symfony/polyfill-php82",
"version": "v1.34.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php82.git",
@@ -1429,7 +1429,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php82/tree/v1.34.0"
"source": "https://github.com/symfony/polyfill-php82/tree/v1.37.0"
},
"funding": [
{

View File

@@ -4,5 +4,5 @@
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
RewriteRule ^(.*)$ index.php [L,E=PATH_INFO:/$1]
</IfModule>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -55,12 +55,17 @@ Route::group(function () {
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/hostnames/refresh/:id', 'cloudflare/hostnames_refresh');
Route::post('/cloudflare/hostnames/txttargets/:id', 'cloudflare/hostnames_txt_targets');
Route::post('/cloudflare/hostnames/batch_add/:id', 'cloudflare/hostnames_batch_add');
Route::post('/cloudflare/hostnames/batch_delete/:id', 'cloudflare/hostnames_batch_delete');
Route::post('/cloudflare/hostnames/batch_update/:id', 'cloudflare/hostnames_batch_update');
Route::post('/cloudflare/fallback/get/:id', 'cloudflare/fallback_get');
Route::post('/cloudflare/fallback/set/:id', 'cloudflare/fallback_set');
Route::post('/cloudflare/fallback/delete/:id', 'cloudflare/fallback_delete');
Route::post('/cloudflare/dcv_delegation_uuid/:id', 'cloudflare/dcv_delegation_uuid');
Route::post('/cloudflare/get_domain_default_line', 'cloudflare/get_domain_default_line');
Route::get('/cloudflare/tunnels/:id', 'cloudflare/tunnels');
Route::post('/cloudflare/tunnels/data/:id', 'cloudflare/tunnels_data');
Route::post('/cloudflare/tunnels/add/:id', 'cloudflare/tunnels_add');
@@ -101,6 +106,8 @@ Route::group(function () {
Route::any('/record/weight/:id', 'domain/weight');
Route::any('/record/alias/:id', 'domain/alias');
Route::get('/record/:id', 'domain/record');
Route::get('/record/smartparse', 'domain/smartparse');
Route::post('/record/quickinfo/:id', 'domain/quickinfo');
Route::get('/dmonitor/overview', 'dmonitor/overview');
Route::post('/dmonitor/task/data', 'dmonitor/task_data');