jeremykendall/php-domain-parser

基于 PHP 实现的公共后缀列表和 IANA 根区域数据库的域名解析。

资助包维护!
nyamsprod

安装: 9,664,270

依赖: 64

建议者: 4

安全: 0

星星: 1,156

观察者: 29

分支: 127

开放问题: 0

6.3.0 2023-02-25 16:31 UTC

README

PHP 域名解析器 是一个基于资源的域名解析器,用 PHP 实现。

Build Status Total Downloads Latest Stable Version Software License

动机

虽然有很多优秀的 URL 解析器和构建器,但能准确解析域名到其子域名、可注册域名、二级域名和公共后缀部分的很少。

以域名 www.pref.okinawa.jp 为例。在这个域名中,公共后缀 部分是 okinawa.jp可注册域名pref.okinawa.jp子域名www二级域名pref
这是无法用正则表达式完成的。

PHP 域名解析器符合

  • 基于精确的公共后缀列表的解析。
  • 基于精确的 IANA 顶级域名列表的解析。

安装

Composer

composer require jeremykendall/php-domain-parser:^6.0

系统要求

您需要

  • PHP >= 7.4,但建议使用最新稳定的 PHP 版本
  • intl 扩展
  • 公共后缀列表数据的一个副本和/或 IANA 顶级域名列表的一个副本。当在生产环境中使用此包时,请参阅管理外部数据源部分以获取更多信息。

使用方法

警告

如果您是从版本 5 升级,请检查升级指南以了解已知问题。

解析域名

此库可以解析域名与以下

  • 公共后缀列表
  • IANA 顶级域名列表

在这两种情况下,都是通过资源实例上实现的 resolve 方法完成的。该方法返回一个表示该过程结果的 Pdp\ResolvedDomain 对象。

对于公共后缀列表,您需要使用以下所示的 Pdp\Rules

<?php 
use Pdp\Rules;
use Pdp\Domain;

$publicSuffixList = Rules::fromPath('/path/to/cache/public-suffix-list.dat');
$domain = Domain::fromIDNA2008('www.PreF.OkiNawA.jP');

$result = $publicSuffixList->resolve($domain);
echo $result->domain()->toString();            //display 'www.pref.okinawa.jp';
echo $result->subDomain()->toString();         //display 'www';
echo $result->secondLevelDomain()->toString(); //display 'pref';
echo $result->registrableDomain()->toString(); //display 'pref.okinawa.jp';
echo $result->suffix()->toString();            //display 'okinawa.jp';
$result->suffix()->isICANN();                  //return true;

对于 IANA 顶级域名列表,使用 Pdp\TopLevelDomains 类代替

<?php

use Pdp\Domain;
use Pdp\TopLevelDomains;

$topLevelDomains = TopLevelDomains::fromPath('/path/to/cache/tlds-alpha-by-domain.txt');
$domain = Domain::fromIDNA2008('www.PreF.OkiNawA.jP');

$result = $topLevelDomains->resolve($domain);
echo $result->domain()->toString();            //display 'www.pref.okinawa.jp';
echo $result->suffix()->toString();            //display 'jp';
echo $result->secondLevelDomain()->toString(); //display 'okinawa';
echo $result->registrableDomain()->toString(); //display 'okinawa.jp';
echo $result->subDomain()->toString();         //display 'www.pref';
echo $result->suffix()->isIANA();              //return true

在发生错误时,会抛出一个扩展自 Pdp\CannotProcessHost 的异常。

resolve 方法总是会返回一个 ResolvedDomain 对象,即使域名语法无效或资源数据中没有找到匹配项。为了解决这个问题,库公开了更严格的方法,即

  • Rules::getCookieDomain
  • Rules::getICANNDomain
  • Rules::getPrivateDomain

对于公共后缀列表,以下方法用于顶级域名列表

  • TopLevelDomains::getIANADomain

这些方法使用与 resolve 方法相同的规则解析域名,但如果找不到有效的有效 TLD 或提交的域名无效,则会抛出异常。

注意

所有这些方法都期望将一个实现Pdp\Host的对象作为唯一参数,但其他类型(例如:stringnull和可转换为字符串的对象)也支持,具体条件如文档中所述。

<?php

use Pdp\Domain;
use Pdp\Rules;
use Pdp\TopLevelDomains;

$publicSuffixList = Rules::fromPath('/path/to/cache/public-suffix-list.dat');
$domain = Domain::fromIDNA2008('qfdsf.unknownTLD');

$publicSuffixList->getICANNDomain($domain);
// will throw because `.unknownTLD` is not part of the ICANN section

$result = $publicSuffixList->getCookieDomain($domain);
$result->suffix()->value();   // returns 'unknownTLD'
$result->suffix()->isKnown(); // returns false
// will not throw because the domain syntax is correct.

$publicSuffixList->getCookieDomain(Domain::fromIDNA2008('com'));
// will not throw because the domain syntax is invalid (ie: does not support public suffix)

$result = $publicSuffixList->resolve(Domain::fromIDNA2008('com'));
$result->suffix()->value();   // returns null
$result->suffix()->isKnown(); // returns false
// will not throw but its public suffix value equal to NULL

$topLevelDomains = TopLevelDomains::fromPath('/path/to/cache/public-suffix-list.dat');
$topLevelDomains->getIANADomain(Domain::fromIDNA2008('com'));
// will not throw because the domain syntax is invalid (ie: does not support public suffix)

要实例化每个域解析器,您可以使用以下命名构造函数

  • fromString:从表示数据源的字符串中实例化解析器;
  • fromPath:通过依赖fopen从本地路径或在线URL实例化解析器;

如果实例化失败,将抛出异常。

警告

在生产环境中,您绝对不应该以这种方式解析域名,至少应该有一个缓存机制来减少外部资源下载。使用公共后缀列表来确定哪些是有效的域名以及哪些不是是非常危险的,可能会因为新顶级域的定期注册而导致错误。如果您想了解顶级域的有效性,您必须使用IANA顶级域列表作为此信息的正确来源,或者使用DNS作为替代。如果您必须使用此库进行上述任何目的,您应该考虑将更新机制集成到您的软件中。更多信息请参阅管理外部数据源部分**

解析后的域名信息。

无论选择哪种方法解析域名,在成功解析后,该包将返回一个Pdp\ResolvedDomain实例。

Pdp\ResolvedDomain不仅装饰了解析后的Pdp\Domain类,还通过单独的方法提供了对域名不同组件的访问。

use Pdp\Domain;
use Pdp\TopLevelDomains;

$domain = Domain::fromIDNA2008('www.PreF.OkiNawA.jP');
/** @var TopLevelDomains $topLevelDomains */
$result = $topLevelDomains->resolve($domain);
echo $result->domain()->toString();            //display 'www.pref.okinawa.jp';
echo $result->suffix()->toString();            //display 'jp';
echo $result->secondLevelDomain()->toString(); //display 'okinawa';
echo $result->registrableDomain()->toString(); //display 'okinawa.jp';
echo $result->subDomain()->toString();         //display 'www.pref';
echo $result->suffix()->isIANA();              //return true

您可以使用以下方法修改返回的Pdp\ResolvedDomain实例

<?php 

use Pdp\Domain;
use Pdp\Rules;

/** @var Rules $publicSuffixList */
$result = $publicSuffixList->resolve(Domain::fromIDNA2008('shop.example.com'));
$altResult = $result
    ->withSubDomain(Domain::fromIDNA2008('foo.bar'))
    ->withSecondLevelDomain(Domain::fromIDNA2008('test'))
    ->withSuffix(Domain::fromIDNA2008('example'));

echo $result->domain()->toString(); //display 'shop.example.com';
$result->suffix()->isKnown();       //return true;

echo $altResult->domain()->toString(); //display 'foo.bar.test.example';
$altResult->suffix()->isKnown();       //return false;

提示

始终优先提交一个Pdp\Suffix对象而不是任何其他支持的类型,以避免意外结果。默认情况下,如果输入不是Pdp\Suffix实例,则生成的公共后缀将被标记为未知。更多信息请参阅公共后缀部分

域名后缀

域有效TLD使用Pdp\Suffix表示。根据数据源,该对象公开有关其来源的不同信息。

<?php 
use Pdp\Domain;
use Pdp\Rules;

/** @var Rules $publicSuffixList */
$suffix = $publicSuffixList->resolve(Domain::fromIDNA2008('example.github.io'))->suffix();

echo $suffix->domain()->toString(); //display 'github.io';
$suffix->isICANN();                 //will return false
$suffix->isPrivate();               //will return true
$suffix->isPublicSuffix();          //will return true
$suffix->isIANA();                  //will return false
$suffix->isKnown();                 //will return true

公共后缀状态取决于其来源

  • isKnown返回true如果值存在于数据资源中。
  • isIANA返回true如果值存在于IANA顶级域列表中。
  • isPublicSuffix返回true如果值存在于公共后缀列表中。
  • isICANN返回true如果值存在于公共后缀列表的ICANN部分。
  • isPrivate返回true如果值存在于公共后缀列表的私人部分。

当通过其命名构造函数实例化Pdp\Suffix对象时,使用相同的信息

<?php 
use Pdp\Suffix;

$iana = Suffix::fromIANA('ac.be');
$icann = Suffix::fromICANN('ac.be');
$private = Suffix::fromPrivate('ac.be');
$unknown = Suffix::fromUnknown('ac.be');

使用Suffix对象而不是字符串或nullResolvedDomain::withSuffix一起使用将确保返回的值始终包含有关公共后缀解析的正确信息。

使用Domain对象而不是字符串或null与命名构造函数一起使用将确保更好地实例化公共后缀对象,更多信息请参阅ASCII和Unicode格式部分

访问和处理域名标签

如果您对在不考虑有效TLD的情况下操作域名标签感兴趣,该库提供了一个专门用于操作域名标签的Domain对象。您可以使用以下方法访问该对象

  • ResolvedDomain::domain方法
  • ResolvedDomain::subDomain方法
  • ResolvedDomain::registrableDomain方法
  • ResolvedDomain::secondLevelDomain方法
  • Suffix::domain方法

Domain对象的用法将在下一节中解释。

<?php
use Pdp\Domain;
use Pdp\Rules;

/** @var Rules $publicSuffixList */
$result = $publicSuffixList->resolve(Domain::from2008('www.bbc.co.uk'));
$domain = $result->domain();
echo $domain->toString(); // display 'www.bbc.co.uk'
count($domain);           // returns 4
$domain->labels();        // returns ['uk', 'co', 'bbc', 'www'];
$domain->label(-1);       // returns 'www'
$domain->label(0);        // returns 'uk'
foreach ($domain as $label) {
   echo $label, PHP_EOL;
}
// display 
// uk
// co
// bbc
// www

$publicSuffixDomain = $result->suffix()->domain();
$publicSuffixDomain->labels(); // returns ['uk', 'co']

您还可以使用以下方法根据其键索引添加或删除标签

<?php 
use Pdp\Domain;
use Pdp\Rules;

/** @var Rules $publicSuffixList */
$domain = $publicSuffixList->resolve(Domain::from2008('www.ExAmpLE.cOM'))->domain();

$newDomain = $domain
    ->withLabel(1, 'com')  //replace 'example' by 'com'
    ->withoutLabel(0, -1)  //remove the first and last labels
    ->append('www')
    ->prepend('docs.example');

echo $domain->toString();           //display 'www.example.com'
echo $newDomain->toString();        //display 'docs.example.com.www'
$newDomain->clear()->labels();      //return []
echo $domain->slice(2)->toString(); //display 'www'

警告

由于定义,域名可以是 null 或字符串。

为了区分这种可能性,对象公开了两种格式化方法:Domain::value,可以是 null 或字符串,以及 Domain::toString,它将始终将域名值转换为字符串。

use Pdp\Domain;

$nullDomain = Domain::fromIDNA2008(null);
$nullDomain->value();    // returns null;
$nullDomain->toString(); // returns '';

$emptyDomain = Domain::fromIDNA2008('');
$emptyDomain->value();    // returns '';
$emptyDomain->toString(); // returns '';

ASCII 和 Unicode 格式。

域名最初只支持 ASCII 字符。如今,它们也可以以 Unicode 表示形式呈现。这两种格式之间的转换是通过符合 UTS#46(也称为 Unicode IDNA 兼容处理)的实现来完成的。域名对象公开了 toAsciitoUnicode 方法,它们返回转换格式的新实例。

<?php 
use Pdp\Rules;

/** @var Rules $publicSuffixList */
$unicodeDomain = $publicSuffixList->resolve('bébé.be')->domain();
echo $unicodeDomain->toString(); // returns 'bébé.be'

$asciiDomain = $publicSuffixList->resolve('xn--bb-bjab.be')->domain();
echo $asciiDomain->toString();  // returns 'xn--bb-bjab.be'

$asciiDomain->toUnicode()->toString() === $unicodeDomain->toString(); //returns true
$unicodeDomain->toAscii()->toString() === $asciiDomain->toString();   //returns true

默认情况下,库使用 IDNA2008 算法在两种格式之间转换域名。仍然可以使用称为 IDNA2003 的旧转换算法。

由于两种算法之间不能直接转换,因此您需要在创建新的域名实例时明确指定将使用哪种算法,这是通过 Pdp\Domain 对象的两种命名构造函数来完成的。

  • Pdp\Domain::fromIDNA2008
  • Pdp\Domain::fromIDNA2003

在任何给定时刻,Pdp\Domain 实例都可以告诉你它是否处于 ASCII 模式。

警告

一旦实例化,就无法知道用于将对象从 ASCII 转换为 Unicode 以及反之亦然的算法。

use Pdp\Domain;

$domain = Domain::fromIDNA2008('faß.de');
echo $domain->value(); // display 'faß.de'
$domain->isAscii();    // return false

$asciiDomain = $domain->toAscii(); 
echo $asciiDomain->value(); // display 'xn--fa-hia.de'
$asciiDomain->isAscii();    // returns true

$domain = Domain::fromIDNA2003('faß.de');
echo $domain->value(); // display 'fass.de'
$domain->isAscii();    // returns true

$asciiDomain = $domain->toAscii();
echo $asciiDomain->value(); // display 'fass.de'
$asciiDomain->isAscii();    // returns true

提示

始终优先提交 Pdp\Domain 对象以进行解析,而不是字符串或可以转换为字符串的对象,以避免意外的格式转换错误/结果。默认情况下,并且在没有信息的情况下,转换使用 IDNA 2008 规则进行。

管理包外部资源

根据您的应用程序,存储资源的机制可能不同,但库附带了一个 可选服务,该服务可以解析域名,而无需持续下载远程数据库所带来的持续网络开销。

Pdp\Storage 命名空间下定义的接口和类使您能够集成资源管理系统,并提供使用 PHP-FIG PSR 接口实现的示例。

使用 PHP-FIG 接口

Pdp\Storage\PsrStorageFactory 允许返回存储实例,这些实例使用 PHP-FIG 发布的标准接口检索、转换和缓存 Public Suffix List 和 IANA 顶级域名列表。

为了按预期工作,Pdp\Storage\PsrStorageFactory 构造函数需要

  • 一个实现 PSR-16 Simple Cache 的库。
  • 一个实现 PSR-17 HTTP Factory 的库。
  • 一个实现 PSR-18 HTTP Client 的库。

在创建新的存储实例时,您将需要

  • 一个 $cachePrefix 参数,您可以可选地向您的缓存索引添加前缀,默认为空字符串;
  • 一个 $ttl 参数,如果您需要设置默认的 $ttl,默认为 null 以使用底层缓存的默认 TTL;

$ttl 参数可以是

  • 一个代表秒数的 int(参见 PSR-16);
  • 一个 DateInterval 对象(参见 PSR-16);
  • 一个代表项目将何时过期的 DateTimeInterface 对象;

该包不提供此类接口的实现,您可以在 packagist 上找到健壮经过实战检验实现

使用提供的工厂刷新资源

备注

这是推荐使用该库的方式

为了本例,我们将使用我们的PSR驱动解决方案,

  • 使用Guzzle HTTP客户端作为我们的PSR-18 HTTP客户端;
  • Guzzle PSR-7包,它提供了使用PSR-17接口创建PSR-7对象的工厂;
  • Symfony缓存组件作为我们的PSR-16缓存实现提供商;

我们将缓存外部来源24小时,在PostgreSQL数据库中。

只要它们实现了所需的PSR接口,您就可以自由使用其他库/解决方案/设置。

<?php 

use GuzzleHttp\Psr7\Request;
use Pdp\Storage\PsrStorageFactory;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Symfony\Component\Cache\Adapter\PdoAdapter;
use Symfony\Component\Cache\Psr16Cache;

$pdo = new PDO(
    'pgsql:host=localhost;port:5432;dbname=testdb', 
    'user', 
    'password', 
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$cache = new Psr16Cache(new PdoAdapter($pdo, 'pdp', 43200));
$client = new GuzzleHttp\Client();
$requestFactory = new class implements RequestFactoryInterface {
    public function createRequest(string $method, $uri): RequestInterface
    {
        return new Request($method, $uri);
    }
};

$cachePrefix = 'pdp_';
$cacheTtl = new DateInterval('P1D');
$factory = new PsrStorageFactory($cache, $client, $requestFactory);
$pslStorage = $factory->createPublicSuffixListStorage($cachePrefix, $cacheTtl);
$rzdStorage = $factory->createTopLevelDomainListStorage($cachePrefix, $cacheTtl);

// if you need to force refreshing the rules 
// before calling them (to use in a refresh script)
// uncomment this part or adapt it to you script logic
// $pslStorage->delete(PsrStorageFactory::PUBLIC_SUFFIX_LIST_URI);
$publicSuffixList = $pslStorage->get(PsrStorageFactory::PUBLIC_SUFFIX_LIST_URI);

// if you need to force refreshing the rules 
// before calling them (to use in a refresh script)
// uncomment this part or adapt it to you script logic
// $rzdStorage->delete(PsrStorageFactory::TOP_LEVEL_DOMAIN_LIST_URI);
$topLevelDomains = $rzdStorage->get(PsrStorageFactory::TOP_LEVEL_DOMAIN_LIST_URI);

备注

请确保将以下代码适配到您自己的应用程序中。以下代码仅作为示例提供,并不保证直接使用即可工作。

警告

您应该使用您的依赖注入容器来避免在应用程序中重复此代码。

自动更新

始终拥有最新的公共后缀列表和顶级域名列表非常重要。
由于执行此类任务严重依赖于您的应用程序设置,因此此库不再提供即插即用的脚本。您可以将上述示例脚本作为实现此类任务的起点。

变更日志

请参阅变更日志,了解自版本5.0.0发布以来所进行的更改。

贡献

欢迎贡献,并将得到充分认可。请参阅贡献指南了解详细信息。

测试

pdp-domain-parser具有

  • PHPUnit测试套件
  • 使用PHPStan的代码分析合规性测试套件。
  • 使用PHP CS Fixer的编码风格合规性测试套件。

要从项目文件夹中运行测试,请运行以下命令。

$ composer test

安全

如果您发现任何安全相关的问题,请通过电子邮件[email protected]联系,而不是使用问题跟踪器。

鸣谢

许可证

MIT许可证(MIT)。请参阅许可证文件获取更多信息。

署名

Pdp\Rules类的一部分是PHP registered-domain-libs的派生作品。我在此项目中包含了一份Apache软件基金会许可证2.0的副本。