新增HE DNS

This commit is contained in:
net909
2026-06-30 16:54:44 +08:00
parent ff8cb42e2d
commit 7f2dfef3ba
3 changed files with 598 additions and 0 deletions
+36
View File
@@ -442,6 +442,41 @@ class DnsHelper
'page' => true,
'add' => false,
],
'henet' => [
'name' => 'HE DNS',
'icon' => 'he.ico',
'note' => '',
'config' => [
'username' => [
'name' => '用户名/邮箱',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'password' => [
'name' => '密码',
'type' => 'input',
'placeholder' => '',
'required' => true,
],
'proxy' => [
'name' => '使用代理服务器',
'type' => 'radio',
'options' => [
'0' => '否',
'1' => '是',
],
'value' => '0'
],
],
'remark' => 0,
'status' => false,
'redirect' => false,
'log' => false,
'weight' => false,
'page' => true,
'add' => false,
],
'spaceship' => [
'name' => 'Spaceship',
'icon' => 'spaceship.ico',
@@ -699,6 +734,7 @@ class DnsHelper
'qingcloud' => ['DEF' => '0', 'CT' => '2', 'CU' => '3', 'CM' => '4', 'AB' => '8'],
'cloudflare' => ['DEF' => '0'],
'namesilo' => ['DEF' => 'default'],
'henet' => ['DEF' => 'default'],
'powerdns' => ['DEF' => 'default'],
'spaceship' => ['DEF' => 'default'],
'aliyunesa' => ['DEF' => '0'],
+562
View File
@@ -0,0 +1,562 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
use Exception;
use think\facade\Cache;
class henet implements DnsInterface
{
private $username;
private $password;
private $baseUrl = 'https://dns.he.net/';
private $error;
private $domain;
private $domainid;
private $proxy;
private $cookie = '';
private $loggedIn = false;
private $cacheKey;
public function __construct($config)
{
$this->username = $config['username'];
$this->password = $config['password'];
$this->domain = $config['domain'];
$this->domainid = $config['domainid'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
$this->cacheKey = 'henet_cookie_' . md5($this->username . '|' . $this->password);
$this->loadCachedSession();
}
public function getError()
{
return $this->error;
}
public function check()
{
return $this->getDomainList() !== false;
}
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20)
{
$html = $this->login();
if ($html === false) {
return false;
}
$list = $this->parseDomains($html);
if (!isNullOrEmpty($KeyWord)) {
$list = array_values(array_filter($list, function ($row) use ($KeyWord) {
return stripos($row['Domain'], $KeyWord) !== false;
}));
}
$total = count($list);
$offset = max(0, ($PageNumber - 1) * $PageSize);
$list = array_slice($list, $offset, $PageSize);
return ['total' => $total, 'list' => $list];
}
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$zoneid = $this->getZoneId();
if ($zoneid === false) {
return false;
}
$html = $this->request('GET', 'index.cgi?' . http_build_query([
'hosted_dns_zoneid' => $zoneid,
'menu' => 'edit_zone',
'hosted_dns_editzone' => '',
]));
if ($html === false) {
return false;
}
$list = $this->parseRecords($html);
if (!isNullOrEmpty($SubDomain)) {
$list = array_values(array_filter($list, function ($row) use ($SubDomain) {
return strcasecmp($row['Name'], $SubDomain) === 0;
}));
} else {
if (!isNullOrEmpty($KeyWord)) {
$list = array_values(array_filter($list, function ($row) use ($KeyWord) {
return stripos($row['Name'], $KeyWord) !== false || stripos($row['Value'], $KeyWord) !== false;
}));
}
if (!isNullOrEmpty($Value)) {
$list = array_values(array_filter($list, function ($row) use ($Value) {
return $row['Value'] == $Value;
}));
}
if (!isNullOrEmpty($Type)) {
$list = array_values(array_filter($list, function ($row) use ($Type) {
return strcasecmp($row['Type'], $Type) === 0;
}));
}
if (!isNullOrEmpty($Status)) {
$list = array_values(array_filter($list, function ($row) use ($Status) {
return $row['Status'] == $Status;
}));
}
}
$total = count($list);
$offset = max(0, ($PageNumber - 1) * $PageSize);
$list = array_slice($list, $offset, $PageSize);
return ['total' => $total, 'list' => $list];
}
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)
{
$records = $this->getDomainRecords(1, 1000);
if ($records === false) {
return false;
}
foreach ($records['list'] as $row) {
if ($row['RecordId'] == $RecordId) {
return $row;
}
}
$this->setError('解析记录不存在');
return false;
}
public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$zoneid = $this->getZoneId();
if ($zoneid === false) {
return false;
}
$params = $this->buildRecordParams($zoneid, '', $Name, $Type, $Value, $TTL, $MX);
$params['hosted_dns_editrecord'] = 'Submit';
$html = $this->request('POST', 'index.cgi', $params);
if ($html !== false && $this->isSuccess($html, 'Successfully added new record')) {
$recordid = $this->findRecordInHtml($html, $Name, $Type, $Value);
return $recordid ? $recordid : true;
}
$this->setError($this->extractMessage($html) ?: '添加解析记录失败');
return false;
}
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$zoneid = $this->getZoneId();
if ($zoneid === false) {
return false;
}
$params = $this->buildRecordParams($zoneid, $RecordId, $Name, $Type, $Value, $TTL, $MX);
$params['hosted_dns_editrecord'] = 'Update';
$html = $this->request('POST', 'index.cgi', $params);
if ($html !== false && $this->isSuccess($html, 'Successfully updated record')) {
return true;
}
$this->setError($this->extractMessage($html) ?: '修改解析记录失败');
return false;
}
public function updateDomainRecordRemark($RecordId, $Remark)
{
return false;
}
public function deleteDomainRecord($RecordId)
{
$zoneid = $this->getZoneId();
if ($zoneid === false) {
return false;
}
$html = $this->request('POST', 'index.cgi', [
'menu' => 'edit_zone',
'hosted_dns_zoneid' => $zoneid,
'hosted_dns_recordid' => $RecordId,
'hosted_dns_editzone' => '1',
'hosted_dns_delrecord' => '1',
'hosted_dns_delconfirm' => 'delete',
]);
if ($html !== false && $this->isSuccess($html, 'Successfully removed record')) {
return true;
}
$this->setError($this->extractMessage($html) ?: '删除解析记录失败');
return false;
}
public function setDomainRecordStatus($RecordId, $Status)
{
return 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 300;
}
public function addDomain($Domain)
{
return false;
}
private function login()
{
if ($this->loggedIn) {
return $this->request('GET', '');
}
$this->request('GET', '', null, false);
$html = $this->request('POST', '', [
'email' => $this->username,
'pass' => $this->password,
], false);
if ($html === false) {
return false;
}
if (stripos($html, 'incorrect') !== false || stripos($html, 'Invalid') !== false) {
$this->setError($this->extractMessage($html) ?: '登录失败,请检查用户名和密码');
return false;
}
$domains = $this->parseDomains($html);
if (empty($domains) && stripos($html, 'hosted_dns_zoneid') === false) {
$this->setError($this->extractMessage($html) ?: '登录失败,未找到域名列表');
return false;
}
$this->loggedIn = true;
$this->saveCachedSession();
return $html;
}
private function getZoneId()
{
if (!isNullOrEmpty($this->domainid)) {
return $this->domainid;
}
if (isNullOrEmpty($this->domain)) {
$this->setError('未指定域名');
return false;
}
$domains = $this->getDomainList($this->domain, 1, 1000);
if ($domains === false) {
return false;
}
foreach ($domains['list'] as $row) {
if (strcasecmp($row['Domain'], $this->domain) === 0) {
$this->domainid = $row['DomainId'];
return $this->domainid;
}
}
$this->setError('域名不存在或无权限访问');
return false;
}
private function buildRecordParams($zoneid, $recordid, $Name, $Type, $Value, $TTL, $MX)
{
return [
'account' => '',
'menu' => 'edit_zone',
'Type' => strtoupper($Type),
'hosted_dns_zoneid' => $zoneid,
'hosted_dns_recordid' => $recordid,
'hosted_dns_editzone' => '1',
'Priority' => strtoupper($Type) == 'MX' ? intval($MX) : '',
'Name' => $this->toFullName($Name),
'Content' => $this->convertValue($Value, $Type),
'TTL' => intval($TTL),
];
}
private function parseDomains($html)
{
$list = [];
$patterns = [
'/onclick="delete_dom\(this\);"[^>]*name="([^"]+)"[^>]*value="(\d+)"/i',
'/name="([^"]+)"[^>]*value="(\d+)"[^>]*onclick="delete_dom\(this\);"/i',
];
foreach ($patterns as $pattern) {
if (!preg_match_all($pattern, $html, $matches, PREG_SET_ORDER)) {
continue;
}
foreach ($matches as $match) {
$domain = html_entity_decode($match[1], ENT_QUOTES);
$zoneid = $match[2];
$list[$zoneid] = [
'DomainId' => $zoneid,
'Domain' => $domain,
'RecordCount' => 0,
];
}
}
if (preg_match_all('/hosted_dns_zoneid=(\d+)[^"\']*["\'][^>]*>\s*([^<]+)\s*</i', $html, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$domain = trim(html_entity_decode($match[2], ENT_QUOTES));
if ($domain !== '' && !isset($list[$match[1]])) {
$list[$match[1]] = [
'DomainId' => $match[1],
'Domain' => $domain,
'RecordCount' => 0,
];
}
}
}
return array_values($list);
}
private function parseRecords($html)
{
$list = [];
if (!preg_match_all('/<tr class="dns_tr" [^>]*>.*?<\/tr>/is', $html, $rows, PREG_SET_ORDER)) {
return $list;
}
foreach ($rows as $row) {
$cells = $this->extractCells($row[0]);
if (count($cells) < 4) continue;
$ttl = is_numeric($cells[4]) ? intval($cells[4]) : 0;
$name = $cells[2];
$type = strtoupper($cells[3]);
$value = $cells[6];
$priority = is_numeric($cells[5]) ? intval($cells[5]) : null;
$list[] = [
'RecordId' => $cells[1],
'Domain' => $this->domain,
'Name' => $this->fromFullName($name),
'Type' => $type,
'Value' => $this->normalizeValue($value, $type),
'Line' => 'default',
'TTL' => $ttl,
'MX' => $priority,
'Status' => stripos($row[0], 'disabled') !== false ? '0' : '1',
'Weight' => null,
'Remark' => null,
'UpdateTime' => null,
];
}
return $list;
}
private function extractCells($html)
{
$cells = [];
if (preg_match_all('/<td[^>]*>(.*?)<\/td>/is', $html, $matches)) {
foreach ($matches[1] as $cell) {
$cell = preg_replace('/<script\b[^>]*>.*?<\/script>/is', '', $cell);
$cell = preg_replace('/<style\b[^>]*>.*?<\/style>/is', '', $cell);
$cell = trim(html_entity_decode(strip_tags($cell), ENT_QUOTES));
$cell = preg_replace('/\s+/', ' ', $cell);
if ($cell !== '') {
$cells[] = $cell;
}
}
}
return $cells;
}
private function findRecordInHtml($html, $Name, $Type, $Value)
{
$records = $this->parseRecords($html);
$name = $this->fromFullName($this->toFullName($Name));
$value = $this->normalizeValue($this->convertValue($Value, $Type), $Type);
foreach ($records as $record) {
if (
strcasecmp($record['Name'], $name) === 0 &&
strcasecmp($record['Type'], $Type) === 0 &&
$record['Value'] == $value
) {
return $record;
}
}
return false;
}
private function toFullName($name)
{
if ($name == '@' || $name == '') {
return $this->domain;
}
if (str_ends_with($name, '.' . $this->domain) || strcasecmp($name, $this->domain) === 0) {
return $name;
}
return $name . '.' . $this->domain;
}
private function fromFullName($name)
{
$name = rtrim($name, '.');
if (strcasecmp($name, $this->domain) === 0) {
return '@';
}
if (str_ends_with(strtolower($name), '.' . strtolower($this->domain))) {
return substr($name, 0, -(strlen($this->domain) + 1));
}
return $name;
}
private function convertValue($value, $type)
{
if (strtoupper($type) == 'TXT') {
return trim($value, '"');
}
return $value;
}
private function normalizeValue($value, $type)
{
$value = trim($value);
if (strtoupper($type) == 'TXT') {
return trim($value, '"');
}
return $value;
}
private function isSuccess($html, $message)
{
return stripos($html, $message) !== false;
}
private function extractMessage($html)
{
if (!$html) {
return null;
}
if (preg_match('/<(?:div|span)[^>]*class="[^"]*(?:error|warn|success|message)[^"]*"[^>]*>(.*?)<\/(?:div|span)>/is', $html, $match)) {
return trim(html_entity_decode(strip_tags($match[1]), ENT_QUOTES));
}
if (preg_match('/(Successfully [^<]+|Error:[^<]+|Invalid [^<]+)/i', $html, $match)) {
return trim(html_entity_decode($match[1], ENT_QUOTES));
}
return null;
}
private function request($method, $path, $params = null, $requireLogin = true, $allowRelogin = true)
{
if ($requireLogin && !$this->loggedIn && $this->login() === false) {
return false;
}
$url = str_starts_with($path, 'http') ? $path : $this->baseUrl . $path;
try {
$response = http_request($url, $params ? http_build_query($params) : null, $this->baseUrl, $this->cookie, null, $this->proxy, $method, 20);
} catch (Exception $e) {
$this->setError($e->getMessage());
return false;
}
$this->storeCookie($response['headers']);
if ($response['code'] >= 400) {
$this->setError('HTTP请求失败:' . $response['code']);
return false;
}
if ($requireLogin && $allowRelogin && $this->isLoginExpired($response['body'])) {
$this->clearCachedSession();
if ($this->login() === false) {
return false;
}
return $this->request($method, $path, $params, true, false);
}
return $response['body'];
}
private function storeCookie($headers)
{
$cookies = [];
if ($this->cookie !== '') {
foreach (explode('; ', $this->cookie) as $cookie) {
$parts = explode('=', $cookie, 2);
if (count($parts) == 2) {
$cookies[$parts[0]] = $parts[1];
}
}
}
foreach ($headers as $name => $values) {
if (strtolower($name) !== 'set-cookie') {
continue;
}
foreach ((array)$values as $value) {
$cookie = explode(';', $value, 2)[0];
$parts = explode('=', $cookie, 2);
if (count($parts) == 2) {
$cookies[$parts[0]] = $parts[1];
}
}
}
$pairs = [];
foreach ($cookies as $name => $value) {
$pairs[] = $name . '=' . $value;
}
$this->cookie = implode('; ', $pairs);
if ($this->loggedIn && $this->cookie !== '') {
$this->saveCachedSession();
}
}
private function loadCachedSession()
{
$cookie = Cache::get($this->cacheKey);
if (is_string($cookie) && $cookie !== '') {
$this->cookie = $cookie;
$this->loggedIn = true;
}
}
private function saveCachedSession()
{
if ($this->cookie !== '') {
Cache::set($this->cacheKey, $this->cookie, 3600);
}
}
private function clearCachedSession()
{
$this->cookie = '';
$this->loggedIn = false;
Cache::delete($this->cacheKey);
}
private function isLoginExpired($html)
{
if (!$html) {
return false;
}
return stripos($html, 'name="email"') !== false
&& stripos($html, 'name="pass"') !== false
&& stripos($html, 'hosted_dns_zoneid') === false;
}
private function setError($message)
{
$this->error = $message;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB