feat: also use registrar RDAP server

This commit is contained in:
Maël Gangloff
2025-10-24 14:09:21 +02:00
parent 2d07dce1ae
commit 1c66821fb6
7 changed files with 213 additions and 32 deletions

View File

@@ -42,6 +42,7 @@
"protonlabs/vobject": "^4.31", "protonlabs/vobject": "^4.31",
"psr/http-client": "^1.0", "psr/http-client": "^1.0",
"runtime/frankenphp-symfony": "^0.2.0", "runtime/frankenphp-symfony": "^0.2.0",
"scienta/doctrine-json-functions": "^6.3",
"symfony/asset": "7.3.*", "symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*", "symfony/asset-mapper": "7.3.*",
"symfony/cache": "7.3.*", "symfony/cache": "7.3.*",

76
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "49fd7fad6776160ff572f3fb9201a8a2", "content-hash": "1db291e0d108c6bb06d447134f1250c6",
"packages": [ "packages": [
{ {
"name": "api-platform/core", "name": "api-platform/core",
@@ -4189,6 +4189,78 @@
}, },
"time": "2024-09-06T08:00:55+00:00" "time": "2024-09-06T08:00:55+00:00"
}, },
{
"name": "scienta/doctrine-json-functions",
"version": "6.3.0",
"source": {
"type": "git",
"url": "https://github.com/ScientaNL/DoctrineJsonFunctions.git",
"reference": "554b2fd281e976a791501fc4753ffd4c5891ec62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ScientaNL/DoctrineJsonFunctions/zipball/554b2fd281e976a791501fc4753ffd4c5891ec62",
"reference": "554b2fd281e976a791501fc4753ffd4c5891ec62",
"shasum": ""
},
"require": {
"doctrine/dbal": "^3.2 || ^4",
"doctrine/lexer": "^2.0 || ^3.0",
"doctrine/orm": "^2.19 || ^3",
"ext-pdo": "*",
"php": "^8.1"
},
"require-dev": {
"doctrine/coding-standard": "^9.0 || ^10.0 || ^11.0 || ^12.0",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^1.12",
"phpstan/phpstan-doctrine": "^1.4",
"phpstan/phpstan-phpunit": "^1.4",
"phpunit/phpunit": "^10.1",
"psalm/plugin-phpunit": "^0.18",
"slevomat/coding-standard": "~8",
"symfony/cache": "^5.4 || ^6.4 || ^7",
"vimeo/psalm": "^5.2",
"webmozart/assert": "^1.11"
},
"suggest": {
"dunglas/doctrine-json-odm": "To serialize / deserialize objects as JSON documents."
},
"type": "library",
"autoload": {
"psr-4": {
"Scienta\\DoctrineJsonFunctions\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Doctrine Json Functions Contributors",
"homepage": "https://github.com/ScientaNL/DoctrineJsonFunctions/contributors"
}
],
"description": "A set of extensions to Doctrine that add support for json query functions.",
"keywords": [
"database",
"doctrine",
"dql",
"json",
"mariadb",
"mysql",
"orm",
"postgres",
"postgresql",
"sqlite"
],
"support": {
"issues": "https://github.com/ScientaNL/DoctrineJsonFunctions/issues",
"source": "https://github.com/ScientaNL/DoctrineJsonFunctions/tree/6.3.0"
},
"time": "2024-11-08T12:33:19+00:00"
},
{ {
"name": "symfony/asset", "name": "symfony/asset",
"version": "v7.3.0", "version": "v7.3.0",
@@ -15319,7 +15391,7 @@
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": ">=8.2", "php": ">=8.4",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-simplexml": "*" "ext-simplexml": "*"

View File

@@ -24,6 +24,9 @@ doctrine:
alias: App alias: App
controller_resolver: controller_resolver:
auto_mapping: false auto_mapping: false
dql:
string_functions:
JSONB_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Postgresql\JsonbContains
when@test: when@test:
doctrine: doctrine:

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251023000555 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('DROP INDEX uniq_e28446850f7084e918020d9');
$this->addSql('ALTER TABLE entity ADD from_accredited_registrar_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE entity ADD CONSTRAINT FK_E2844687CB19E6A FOREIGN KEY (from_accredited_registrar_id) REFERENCES icann_accreditation (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_E2844687CB19E6A ON entity (from_accredited_registrar_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_E28446850F7084E918020D97CB19E6A ON entity (tld_id, handle, from_accredited_registrar_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE entity DROP CONSTRAINT FK_E2844687CB19E6A');
$this->addSql('DROP INDEX IDX_E2844687CB19E6A');
$this->addSql('DROP INDEX UNIQ_E28446850F7084E918020D97CB19E6A');
$this->addSql('ALTER TABLE entity DROP from_accredited_registrar_id');
$this->addSql('CREATE UNIQUE INDEX uniq_e28446850f7084e918020d9 ON entity (tld_id, handle)');
}
}

View File

@@ -13,7 +13,7 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
#[ORM\Entity(repositoryClass: EntityRepository::class)] #[ORM\Entity(repositoryClass: EntityRepository::class)]
#[ORM\UniqueConstraint( #[ORM\UniqueConstraint(
columns: ['tld_id', 'handle'] columns: ['tld_id', 'handle', 'from_accredited_registrar_id']
)] )]
#[ApiResource( #[ApiResource(
operations: [ operations: [
@@ -96,6 +96,9 @@ class Entity
#[Groups(['entity:list', 'entity:item', 'domain:item'])] #[Groups(['entity:list', 'entity:item', 'domain:item'])]
private ?IcannAccreditation $icannAccreditation = null; private ?IcannAccreditation $icannAccreditation = null;
#[ORM\ManyToOne]
private ?IcannAccreditation $fromAccreditedRegistrar = null;
public function __construct() public function __construct()
{ {
$this->domainEntities = new ArrayCollection(); $this->domainEntities = new ArrayCollection();
@@ -264,4 +267,16 @@ class Entity
return $this; return $this;
} }
public function getFromAccreditedRegistrar(): ?IcannAccreditation
{
return $this->fromAccreditedRegistrar;
}
public function setFromAccreditedRegistrar(?IcannAccreditation $fromAccreditedRegistrar): static
{
$this->fromAccreditedRegistrar = $fromAccreditedRegistrar;
return $this;
}
} }

View File

@@ -29,6 +29,20 @@ class DomainEntityRepository extends ServiceEntityRepository
->getQuery()->execute(); ->getQuery()->execute();
} }
public function getDomainEntityFromDomainAndRoles(Domain $domain, array $roles)
{
return $this->createQueryBuilder('de')
->select()
->where('de.deletedAt IS NULL')
->andWhere('de.domain = :domain')
->andWhere('JSONB_CONTAINS(de.roles, :roles) = true')
->setMaxResults(1)
->getQuery()
->setParameter('domain', $domain)
->setParameter('roles', json_encode($roles))
->getOneOrNullResult();
}
// /** // /**
// * @return DomainEntity[] Returns an array of DomainEntity objects // * @return DomainEntity[] Returns an array of DomainEntity objects
// */ // */

View File

@@ -12,6 +12,7 @@ use App\Entity\DomainEvent;
use App\Entity\DomainStatus; use App\Entity\DomainStatus;
use App\Entity\Entity; use App\Entity\Entity;
use App\Entity\EntityEvent; use App\Entity\EntityEvent;
use App\Entity\IcannAccreditation;
use App\Entity\Nameserver; use App\Entity\Nameserver;
use App\Entity\NameserverEntity; use App\Entity\NameserverEntity;
use App\Entity\RdapServer; use App\Entity\RdapServer;
@@ -49,6 +50,7 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
class RDAPService class RDAPService
{ {
public const ENTITY_HANDLE_BLACKLIST = [ public const ENTITY_HANDLE_BLACKLIST = [
'REDACTED',
'REDACTED_FOR_PRIVACY', 'REDACTED_FOR_PRIVACY',
'ANO00-FRNIC', 'ANO00-FRNIC',
'not applicable', 'not applicable',
@@ -117,7 +119,7 @@ class RDAPService
* @throws UnknownRdapServerException * @throws UnknownRdapServerException
* @throws \Exception * @throws \Exception
*/ */
public function registerDomain(string $fqdn): Domain public function registerDomain(string $fqdn, ?IcannAccreditation $icannAccreditation = null): Domain
{ {
$idnDomain = RDAPService::convertToIdn($fqdn); $idnDomain = RDAPService::convertToIdn($fqdn);
$tld = $this->getTld($idnDomain); $tld = $this->getTld($idnDomain);
@@ -126,10 +128,11 @@ class RDAPService
'ldhName' => $idnDomain, 'ldhName' => $idnDomain,
]); ]);
$rdapServer = $this->fetchRdapServer($tld);
$domain = $this->domainRepository->findOneBy(['ldhName' => $idnDomain]); $domain = $this->domainRepository->findOneBy(['ldhName' => $idnDomain]);
$rdapData = $this->fetchRdapResponse($rdapServer, $idnDomain, $domain); $rdapServer = $icannAccreditation ? (new RdapServer())->setUrl($icannAccreditation->getRdapBaseUrl()) : $this->fetchRdapServer($tld);
$rdapData = $this->fetchRdapResponse($rdapServer, $idnDomain, $domain, null === $icannAccreditation);
$this->em->beginTransaction(); $this->em->beginTransaction();
if (null === $domain) { if (null === $domain) {
@@ -139,25 +142,49 @@ class RDAPService
$this->em->lock($domain, LockMode::PESSIMISTIC_WRITE); $this->em->lock($domain, LockMode::PESSIMISTIC_WRITE);
$this->updateDomainStatus($domain, $rdapData); if (null === $icannAccreditation) {
$this->updateDomainStatus($domain, $rdapData);
if (in_array('free', $domain->getStatus())) { if (in_array('free', $domain->getStatus())) {
throw DomainNotFoundException::fromDomain($idnDomain); throw DomainNotFoundException::fromDomain($idnDomain);
}
$domain
->setRdapServer($rdapServer)
->setDelegationSigned(isset($rdapData['secureDNS']['delegationSigned']) && $rdapData['secureDNS']['delegationSigned']);
$this->updateDomainHandle($domain, $rdapData);
$this->updateDomainEvents($domain, $rdapData);
$this->updateDomainEntities($domain, $rdapData);
$this->updateDomainNameservers($domain, $rdapData);
$this->updateDomainDsData($domain, $rdapData);
$domain->setDeleted(false)->updateTimestamps();
/** @var ?DomainEntity $registrar */
$registrar = $this->domainEntityRepository->getDomainEntityFromDomainAndRoles($domain, ['registrar']);
if (null !== $registrar?->getEntity()?->getIcannAccreditation()?->getRdapBaseUrl()) {
try {
$this->registerDomain($idnDomain, $registrar->getEntity()->getIcannAccreditation());
} catch (DomainNotFoundException) {
$this->logger->warning('Domain name exists for the registry but not for the registrar', [
'ldhName' => $domain->getLdhName(),
'icannAccreditation' => $registrar->getEntity()->getIcannAccreditation()->getId(),
]);
} catch (\Throwable) {
$this->logger->error('Domain name cannot be updated from the registrar RDAP server', [
'ldhName' => $domain->getLdhName(),
'icannAccreditation' => $registrar->getEntity()->getIcannAccreditation()->getId(),
]);
}
}
} else {
$this->updateDomainEntities($domain, $rdapData, $icannAccreditation);
} }
$domain
->setRdapServer($rdapServer)
->setDelegationSigned(isset($rdapData['secureDNS']['delegationSigned']) && $rdapData['secureDNS']['delegationSigned']);
$this->updateDomainHandle($domain, $rdapData);
$this->updateDomainEvents($domain, $rdapData);
$this->updateDomainEntities($domain, $rdapData);
$this->updateDomainNameservers($domain, $rdapData);
$this->updateDomainDsData($domain, $rdapData);
$domain->setDeleted(false)->updateTimestamps();
$this->em->flush(); $this->em->flush();
$this->em->commit(); $this->em->commit();
@@ -240,7 +267,7 @@ class RDAPService
* @throws ClientExceptionInterface * @throws ClientExceptionInterface
* @throws \Exception * @throws \Exception
*/ */
private function fetchRdapResponse(RdapServer $rdapServer, string $idnDomain, ?Domain $domain): array private function fetchRdapResponse(RdapServer $rdapServer, string $idnDomain, ?Domain $domain, bool $handleRdapException = true): array
{ {
$rdapServerUrl = $rdapServer->getUrl(); $rdapServerUrl = $rdapServer->getUrl();
$this->logger->info('An RDAP query to update this domain name will be made', [ $this->logger->info('An RDAP query to update this domain name will be made', [
@@ -254,7 +281,10 @@ class RDAPService
return $req->toArray(); return $req->toArray();
} catch (\Exception $e) { } catch (\Exception $e) {
throw $this->handleRdapException($e, $idnDomain, $domain, $req ?? null); if ($handleRdapException) {
throw $this->handleRdapException($e, $idnDomain, $domain, $req ?? null);
}
throw DomainNotFoundException::fromDomain($idnDomain);
} finally { } finally {
if ($this->influxdbEnabled && isset($req)) { if ($this->influxdbEnabled && isset($req)) {
$this->influxService->addRdapQueryPoint($rdapServer, $idnDomain, $req->getInfo()); $this->influxService->addRdapQueryPoint($rdapServer, $idnDomain, $req->getInfo());
@@ -392,9 +422,11 @@ class RDAPService
/** /**
* @throws \Exception * @throws \Exception
*/ */
private function updateDomainEntities(Domain $domain, array $rdapData): void private function updateDomainEntities(Domain $domain, array $rdapData, ?IcannAccreditation $icannAccreditation = null): void
{ {
$this->domainEntityRepository->setDomainEntityAsDeleted($domain); if (!$icannAccreditation) {
$this->domainEntityRepository->setDomainEntityAsDeleted($domain);
}
if (!isset($rdapData['entities']) || !is_array($rdapData['entities'])) { if (!isset($rdapData['entities']) || !is_array($rdapData['entities'])) {
return; return;
@@ -402,7 +434,11 @@ class RDAPService
foreach ($rdapData['entities'] as $rdapEntity) { foreach ($rdapData['entities'] as $rdapEntity) {
$roles = $this->extractEntityRoles($rdapData['entities'], $rdapEntity); $roles = $this->extractEntityRoles($rdapData['entities'], $rdapEntity);
$entity = $this->registerEntity($rdapEntity, $roles, $domain->getLdhName(), $domain->getTld()); if ($domain->getDomainEntities()->findFirst(fn (int $i, DomainEntity $de) => count(array_intersect($de->getRoles(), $roles)) > 0)) {
continue;
}
$entity = $this->registerEntity($rdapEntity, $roles, $domain->getLdhName(), $domain->getTld(), $icannAccreditation);
$domainEntity = $this->domainEntityRepository->findOneBy([ $domainEntity = $this->domainEntityRepository->findOneBy([
'domain' => $domain, 'domain' => $domain,
@@ -513,8 +549,8 @@ class RDAPService
? $targetEntity['handle'] === $e['handle'] ? $targetEntity['handle'] === $e['handle']
: ( : (
isset($targetEntity['vcardArray']) && isset($e['vcardArray']) isset($targetEntity['vcardArray']) && isset($e['vcardArray'])
? $targetEntity['vcardArray'] === $e['vcardArray'] ? $targetEntity['vcardArray'] === $e['vcardArray']
: $targetEntity === $e : $targetEntity === $e
) )
) )
); );
@@ -529,7 +565,7 @@ class RDAPService
/** /**
* @throws \Exception * @throws \Exception
*/ */
private function registerEntity(array $rdapEntity, array $roles, string $domain, Tld $tld): Entity private function registerEntity(array $rdapEntity, array $roles, string $domain, Tld $tld, ?IcannAccreditation $fromAccreditedRegistrar = null): Entity
{ {
/* /*
* If there is no number to identify the entity, one is generated from the domain name and the roles associated with this entity * If there is no number to identify the entity, one is generated from the domain name and the roles associated with this entity
@@ -547,6 +583,7 @@ class RDAPService
$entity = $this->entityRepository->findOneBy([ $entity = $this->entityRepository->findOneBy([
'handle' => $rdapEntity['handle'], 'handle' => $rdapEntity['handle'],
'tld' => $tld, 'tld' => $tld,
'fromAccreditedRegistrar' => $fromAccreditedRegistrar,
]); ]);
if (null === $entity) { if (null === $entity) {
@@ -574,7 +611,7 @@ class RDAPService
} }
} }
$entity->setHandle($rdapEntity['handle'])->setIcannAccreditation($icannAccreditation); $entity->setHandle($rdapEntity['handle'])->setIcannAccreditation($icannAccreditation)->setFromAccreditedRegistrar($fromAccreditedRegistrar);
if (isset($rdapEntity['remarks']) && is_array($rdapEntity['remarks'])) { if (isset($rdapEntity['remarks']) && is_array($rdapEntity['remarks'])) {
$entity->setRemarks($rdapEntity['remarks']); $entity->setRemarks($rdapEntity['remarks']);