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",
"psr/http-client": "^1.0",
"runtime/frankenphp-symfony": "^0.2.0",
"scienta/doctrine-json-functions": "^6.3",
"symfony/asset": "7.3.*",
"symfony/asset-mapper": "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",
"This file is @generated automatically"
],
"content-hash": "49fd7fad6776160ff572f3fb9201a8a2",
"content-hash": "1db291e0d108c6bb06d447134f1250c6",
"packages": [
{
"name": "api-platform/core",
@@ -4189,6 +4189,78 @@
},
"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",
"version": "v7.3.0",
@@ -15319,7 +15391,7 @@
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": ">=8.2",
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"ext-simplexml": "*"

View File

@@ -24,6 +24,9 @@ doctrine:
alias: App
controller_resolver:
auto_mapping: false
dql:
string_functions:
JSONB_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Postgresql\JsonbContains
when@test:
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\UniqueConstraint(
columns: ['tld_id', 'handle']
columns: ['tld_id', 'handle', 'from_accredited_registrar_id']
)]
#[ApiResource(
operations: [
@@ -96,6 +96,9 @@ class Entity
#[Groups(['entity:list', 'entity:item', 'domain:item'])]
private ?IcannAccreditation $icannAccreditation = null;
#[ORM\ManyToOne]
private ?IcannAccreditation $fromAccreditedRegistrar = null;
public function __construct()
{
$this->domainEntities = new ArrayCollection();
@@ -264,4 +267,16 @@ class Entity
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();
}
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
// */

View File

@@ -12,6 +12,7 @@ use App\Entity\DomainEvent;
use App\Entity\DomainStatus;
use App\Entity\Entity;
use App\Entity\EntityEvent;
use App\Entity\IcannAccreditation;
use App\Entity\Nameserver;
use App\Entity\NameserverEntity;
use App\Entity\RdapServer;
@@ -49,6 +50,7 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
class RDAPService
{
public const ENTITY_HANDLE_BLACKLIST = [
'REDACTED',
'REDACTED_FOR_PRIVACY',
'ANO00-FRNIC',
'not applicable',
@@ -117,7 +119,7 @@ class RDAPService
* @throws UnknownRdapServerException
* @throws \Exception
*/
public function registerDomain(string $fqdn): Domain
public function registerDomain(string $fqdn, ?IcannAccreditation $icannAccreditation = null): Domain
{
$idnDomain = RDAPService::convertToIdn($fqdn);
$tld = $this->getTld($idnDomain);
@@ -126,10 +128,11 @@ class RDAPService
'ldhName' => $idnDomain,
]);
$rdapServer = $this->fetchRdapServer($tld);
$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();
if (null === $domain) {
@@ -139,25 +142,49 @@ class RDAPService
$this->em->lock($domain, LockMode::PESSIMISTIC_WRITE);
$this->updateDomainStatus($domain, $rdapData);
if (null === $icannAccreditation) {
$this->updateDomainStatus($domain, $rdapData);
if (in_array('free', $domain->getStatus())) {
throw DomainNotFoundException::fromDomain($idnDomain);
if (in_array('free', $domain->getStatus())) {
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->commit();
@@ -240,7 +267,7 @@ class RDAPService
* @throws ClientExceptionInterface
* @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();
$this->logger->info('An RDAP query to update this domain name will be made', [
@@ -254,7 +281,10 @@ class RDAPService
return $req->toArray();
} 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 {
if ($this->influxdbEnabled && isset($req)) {
$this->influxService->addRdapQueryPoint($rdapServer, $idnDomain, $req->getInfo());
@@ -392,9 +422,11 @@ class RDAPService
/**
* @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'])) {
return;
@@ -402,7 +434,11 @@ class RDAPService
foreach ($rdapData['entities'] as $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([
'domain' => $domain,
@@ -513,8 +549,8 @@ class RDAPService
? $targetEntity['handle'] === $e['handle']
: (
isset($targetEntity['vcardArray']) && isset($e['vcardArray'])
? $targetEntity['vcardArray'] === $e['vcardArray']
: $targetEntity === $e
? $targetEntity['vcardArray'] === $e['vcardArray']
: $targetEntity === $e
)
)
);
@@ -529,7 +565,7 @@ class RDAPService
/**
* @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
@@ -547,6 +583,7 @@ class RDAPService
$entity = $this->entityRepository->findOneBy([
'handle' => $rdapEntity['handle'],
'tld' => $tld,
'fromAccreditedRegistrar' => $fromAccreditedRegistrar,
]);
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'])) {
$entity->setRemarks($rdapEntity['remarks']);