feat: add IANA fields for accredited registrars

This commit is contained in:
Maël Gangloff 2025-09-10 21:35:43 +02:00
parent f9ae2ac5e1
commit 28fb5f2fc3
No known key found for this signature in database
GPG Key ID: 11FDC81C24A7F629
6 changed files with 207 additions and 18 deletions

View File

@ -0,0 +1,48 @@
<?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 Version20250910171544 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add columns for IANA fields in the entity table';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE entity ADD registrar_name_iana VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE entity ADD rdap_base_url_iana VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE entity ADD status_iana VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE entity ADD updated_iana DATE DEFAULT NULL');
$this->addSql('ALTER TABLE entity ADD date_iana DATE DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN entity.updated_iana IS \'(DC2Type:date_immutable)\'');
$this->addSql('COMMENT ON COLUMN entity.date_iana IS \'(DC2Type:date_immutable)\'');
$this->addSql("DELETE FROM domain_entity de USING entity e WHERE de.entity_uid = e.id AND e.handle ~ '^[0-9]+$'");
$this->addSql("DELETE FROM entity_event ev USING entity e WHERE ev.entity_uid = e.id AND e.handle ~ '^[0-9]+$'");
$this->addSql("DELETE FROM nameserver_entity ne USING entity e WHERE ne.entity_uid = e.id AND e.handle ~ '^[0-9]+$'");
$this->addSql("DELETE FROM entity WHERE handle ~ '^[0-9]+$'");
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE entity DROP registrar_name_iana');
$this->addSql('ALTER TABLE entity DROP rdap_base_url_iana');
$this->addSql('ALTER TABLE entity DROP status_iana');
$this->addSql('ALTER TABLE entity DROP updated_iana');
$this->addSql('ALTER TABLE entity DROP date_iana');
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Config;
enum RegistrarStatus: string
{
case Reserved = 'Reserved';
case Accredited = 'Accredited';
case Terminated = 'Terminated';
}

View File

@ -4,9 +4,11 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Get;
use App\Config\RegistrarStatus;
use App\Repository\EntityRepository; use App\Repository\EntityRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\SerializedName; use Symfony\Component\Serializer\Attribute\SerializedName;
@ -86,6 +88,25 @@ class Entity
#[Groups(['entity:item', 'domain:item'])] #[Groups(['entity:item', 'domain:item'])]
private ?array $remarks = null; private ?array $remarks = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['entity:item', 'domain:item'])]
private ?string $registrarNameIANA = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['entity:item', 'domain:item'])]
private ?string $rdapBaseUrlIANA = null;
#[ORM\Column(nullable: true, enumType: RegistrarStatus::class)]
#[Groups(['entity:item', 'domain:item'])]
private ?RegistrarStatus $statusIANA = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $updatedIANA = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $dateIANA = null;
public function __construct() public function __construct()
{ {
$this->domainEntities = new ArrayCollection(); $this->domainEntities = new ArrayCollection();
@ -242,4 +263,64 @@ class Entity
return $this; return $this;
} }
public function getRegistrarNameIANA(): ?string
{
return $this->registrarNameIANA;
}
public function setRegistrarNameIANA(?string $registrarNameIANA): static
{
$this->registrarNameIANA = $registrarNameIANA;
return $this;
}
public function getRdapBaseUrlIANA(): ?string
{
return $this->rdapBaseUrlIANA;
}
public function setRdapBaseUrlIANA(?string $rdapBaseUrlIANA): static
{
$this->rdapBaseUrlIANA = $rdapBaseUrlIANA;
return $this;
}
public function getStatusIANA(): ?RegistrarStatus
{
return $this->statusIANA;
}
public function setStatusIANA(?RegistrarStatus $statusIANA): static
{
$this->statusIANA = $statusIANA;
return $this;
}
public function getUpdatedIANA(): ?\DateTimeImmutable
{
return $this->updatedIANA;
}
public function setUpdatedIANA(?\DateTimeImmutable $updatedIANA): static
{
$this->updatedIANA = $updatedIANA;
return $this;
}
public function getDateIANA(): ?\DateTimeImmutable
{
return $this->dateIANA;
}
public function setDateIANA(?\DateTimeImmutable $dateIANA): static
{
$this->dateIANA = $dateIANA;
return $this;
}
} }

View File

@ -115,12 +115,13 @@ class Tld
public function getTld(): ?string public function getTld(): ?string
{ {
return '' === $this->tld ? '.' : $this->tld; return $this->tld;
} }
public function setTld(string $tld): static public function setTld(string $tld): static
{ {
$this->tld = RDAPService::convertToIdn($tld); $this->tld = RDAPService::convertToIdn($tld);
if($this->tld === '') $this->tld = '.';
return $this; return $this;
} }

View File

@ -65,6 +65,13 @@ final readonly class UpdateRdapServersHandler
$throws[] = $throwable; $throws[] = $throwable;
} }
try {
$this->RDAPService->updateRegistrarListIANA();
} catch (\Throwable $throwable) {
$throws[] = $throwable;
}
if (!empty($throws)) { if (!empty($throws)) {
throw $throws[0]; throw $throws[0];
} }

View File

@ -5,6 +5,7 @@ namespace App\Service;
use App\Config\DnsKey\Algorithm; use App\Config\DnsKey\Algorithm;
use App\Config\DnsKey\DigestType; use App\Config\DnsKey\DigestType;
use App\Config\EventAction; use App\Config\EventAction;
use App\Config\RegistrarStatus;
use App\Config\TldType; use App\Config\TldType;
use App\Entity\DnsKey; use App\Entity\DnsKey;
use App\Entity\Domain; use App\Entity\Domain;
@ -94,11 +95,6 @@ readonly class RDAPService
'Private', 'Private',
]; ];
/* @see https://www.iana.org/assignments/registrar-ids/registrar-ids.xhtml */
public const IANA_RESERVED_IDS = [
1, 3, 8, 119, 365, 376, 9994, 9995, 9996, 9997, 9998, 9999, 10009, 4000001, 8888888,
];
public function __construct(private HttpClientInterface $client, public function __construct(private HttpClientInterface $client,
private EntityRepository $entityRepository, private EntityRepository $entityRepository,
private DomainRepository $domainRepository, private DomainRepository $domainRepository,
@ -551,24 +547,31 @@ readonly class RDAPService
$entity = $this->entityRepository->findOneBy([ $entity = $this->entityRepository->findOneBy([
'handle' => $rdapEntity['handle'], 'handle' => $rdapEntity['handle'],
'tld' => is_numeric($rdapEntity['handle']) ? null : $tld, 'tld' => $tld,
]); ]);
if(null === $entity) { if(null === $entity) {
$entity = new Entity(); $entity = $this->entityRepository->findOneBy([
'handle' => $rdapEntity['handle'],
'tld' => null,
]);
}
if (null === $entity) {
$entity = (new Entity())->setTld($tld);
$this->logger->info('The entity {handle} was not known to this Domain Watchdog instance.', [ $this->logger->info('The entity {handle} was not known to this Domain Watchdog instance.', [
'handle' => $rdapEntity['handle'], 'handle' => $rdapEntity['handle'],
]); ]);
} }
$entity->setHandle($rdapEntity['handle'])->setTld(is_numeric($rdapEntity['handle']) ? null : $tld); $entity->setHandle($rdapEntity['handle']);
if (isset($rdapEntity['remarks']) && is_array($rdapEntity['remarks']) && !is_numeric($rdapEntity['handle'])) { if (isset($rdapEntity['remarks']) && is_array($rdapEntity['remarks']) && $entity->getStatusIANA() === null) {
$entity->setRemarks($rdapEntity['remarks']); $entity->setRemarks($rdapEntity['remarks']);
} }
if (isset($rdapEntity['vcardArray']) && !in_array($rdapEntity['handle'], self::IANA_RESERVED_IDS)) { if (isset($rdapEntity['vcardArray']) && $entity->getStatusIANA() === null) {
if (empty($entity->getJCard())) { if (empty($entity->getJCard())) {
if (!array_key_exists('elements', $rdapEntity['vcardArray'])) { if (!array_key_exists('elements', $rdapEntity['vcardArray'])) {
$entity->setJCard($rdapEntity['vcardArray']); $entity->setJCard($rdapEntity['vcardArray']);
@ -602,7 +605,7 @@ readonly class RDAPService
} }
} }
if ($isIANAid || !isset($rdapEntity['events']) || in_array($rdapEntity['handle'], self::IANA_RESERVED_IDS)) { if ($isIANAid || !isset($rdapEntity['events']) || $entity->getStatusIANA() !== null) {
return $entity; return $entity;
} }
@ -704,22 +707,26 @@ readonly class RDAPService
{ {
foreach ($dnsRoot['services'] as $service) { foreach ($dnsRoot['services'] as $service) {
foreach ($service[0] as $tld) { foreach ($service[0] as $tld) {
if ('' === $tld && null === $this->tldRepository->findOneBy(['tld' => $tld])) { if ('.' === $tld && null === $this->tldRepository->findOneBy(['tld' => $tld])) {
$this->em->persist((new Tld())->setTld('')->setType(TldType::root)); $this->em->persist((new Tld())->setTld('.')->setType(TldType::root));
$this->em->flush(); $this->em->flush();
} }
$tldReference = $this->em->getReference(Tld::class, $tld); $tldEntity = $this->tldRepository->findOneBy(['tld' => $tld]);
if($tldEntity === null) {
$tldEntity = (new Tld())->setTld($tld)->setType(TldType::gTLD);
$this->em->persist($tldEntity);
}
foreach ($service[1] as $rdapServerUrl) { foreach ($service[1] as $rdapServerUrl) {
$server = $this->rdapServerRepository->findOneBy(['tld' => $tldReference, 'url' => $rdapServerUrl]); $server = $this->rdapServerRepository->findOneBy(['tld' => $tldEntity->getTld(), 'url' => $rdapServerUrl]);
if (null === $server) { if (null === $server) {
$server = new RdapServer(); $server = new RdapServer();
} }
$server $server
->setTld($tldReference) ->setTld($tldEntity)
->setUrl($rdapServerUrl) ->setUrl($rdapServerUrl)
->setUpdatedAt(new \DateTimeImmutable($dnsRoot['publication'] ?? 'now')); ->setUpdatedAt(new \DateTimeImmutable($dnsRoot['publication'] ?? 'now'));
@ -792,6 +799,41 @@ readonly class RDAPService
$this->em->flush(); $this->em->flush();
} }
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
* @throws \Exception
*/
public function updateRegistrarListIANA(): void
{
$this->logger->info('Start of retrieval of the list of Registrar IDs according to IANA.');
$registrarList = $this->client->request(
'GET', 'https://www.iana.org/assignments/registrar-ids/registrar-ids.xml'
);
$data = new \SimpleXMLElement($registrarList->getContent());
foreach($data->registry->record as $registrar) {
$entity = $this->entityRepository->findOneBy(['handle' => $registrar->value, 'tld' => null]);
if($entity === null) $entity = new Entity();
$entity
->setHandle(strval($registrar->value))->setTld(null)
->setRegistrarNameIANA(strval($registrar->name))
->setStatusIANA(RegistrarStatus::from(strval($registrar->status)))
->setRdapBaseUrlIANA($registrar->rdapurl->count() ? strval($registrar->rdapurl->server) : null)
->setUpdatedIANA($registrar->attributes()->updated !== null ? new \DateTimeImmutable(strval($registrar->attributes()->updated)) : null)
->setDateIANA($registrar->attributes()->date !== null ? new \DateTimeImmutable(strval($registrar->attributes()->date)) : null)
->setJCard(["vcard", [["version", [], "text", "4.0"], ["fn", [], "text", $entity->getRegistrarNameIANA()]]])
->setRemarks(null);
$this->em->persist($entity);
}
$this->em->flush();
}
private function getTldType(string $tld): ?TldType private function getTldType(string $tld): ?TldType
{ {
if (in_array(strtolower($tld), self::ISO_TLD_EXCEPTION)) { if (in_array(strtolower($tld), self::ISO_TLD_EXCEPTION)) {
@ -835,7 +877,7 @@ readonly class RDAPService
if (null === $gtTldEntity) { if (null === $gtTldEntity) {
$gtTldEntity = new Tld(); $gtTldEntity = new Tld();
$gtTldEntity->setTld($gTld['gTLD']); $gtTldEntity->setTld($gTld['gTLD'])->setType(TldType::gTLD);
$this->logger->notice('New gTLD detected according to ICANN ({tld}).', [ $this->logger->notice('New gTLD detected according to ICANN ({tld}).', [
'tld' => $gTld['gTLD'], 'tld' => $gTld['gTLD'],
]); ]);