diff --git a/migrations/Version20250910171544.php b/migrations/Version20250910171544.php new file mode 100644 index 0000000..95df1a7 --- /dev/null +++ b/migrations/Version20250910171544.php @@ -0,0 +1,46 @@ +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'); + } +} diff --git a/migrations/Version20250910201456.php b/migrations/Version20250910201456.php new file mode 100644 index 0000000..3424ca3 --- /dev/null +++ b/migrations/Version20250910201456.php @@ -0,0 +1,54 @@ +addSql('ALTER TABLE entity ADD iana_registrar_name VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE entity ADD iana_rdap_base_url VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE entity ADD iana_status VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE entity ADD iana_updated DATE DEFAULT NULL'); + $this->addSql('ALTER TABLE entity ADD iana_date DATE DEFAULT NULL'); + $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'); + $this->addSql('COMMENT ON COLUMN entity.iana_updated IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN entity.iana_date IS \'(DC2Type:date_immutable)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $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('ALTER TABLE entity DROP iana_registrar_name'); + $this->addSql('ALTER TABLE entity DROP iana_rdap_base_url'); + $this->addSql('ALTER TABLE entity DROP iana_status'); + $this->addSql('ALTER TABLE entity DROP iana_updated'); + $this->addSql('ALTER TABLE entity DROP iana_date'); + $this->addSql('COMMENT ON COLUMN entity.updated_iana IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN entity.date_iana IS \'(DC2Type:date_immutable)\''); + } +} diff --git a/src/Config/RegistrarStatus.php b/src/Config/RegistrarStatus.php new file mode 100644 index 0000000..9cc9ace --- /dev/null +++ b/src/Config/RegistrarStatus.php @@ -0,0 +1,10 @@ +domainEntities = new ArrayCollection(); $this->nameserverEntities = new ArrayCollection(); $this->events = new ArrayCollection(); + $this->ianaAccreditation = new IanaAccreditation(); } public function getHandle(): ?string @@ -242,4 +247,14 @@ class Entity return $this; } + + public function getIanaAccreditation(): IanaAccreditation + { + return $this->ianaAccreditation; + } + + public function setIanaAccreditation(IanaAccreditation $ianaAccreditation): void + { + $this->ianaAccreditation = $ianaAccreditation; + } } diff --git a/src/Entity/IanaAccreditation.php b/src/Entity/IanaAccreditation.php new file mode 100644 index 0000000..72928f1 --- /dev/null +++ b/src/Entity/IanaAccreditation.php @@ -0,0 +1,93 @@ +registrarName; + } + + public function setRegistrarName(?string $registrarName): static + { + $this->registrarName = $registrarName; + + return $this; + } + + public function getRdapBaseUrl(): ?string + { + return $this->rdapBaseUrl; + } + + public function setRdapBaseUrl(?string $rdapBaseUrl): static + { + $this->rdapBaseUrl = $rdapBaseUrl; + + return $this; + } + + public function getStatus(): ?RegistrarStatus + { + return $this->status; + } + + public function setStatus(?RegistrarStatus $status): static + { + $this->status = $status; + + return $this; + } + + public function getUpdated(): ?\DateTimeImmutable + { + return $this->updated; + } + + public function setUpdated(?\DateTimeImmutable $updated): static + { + $this->updated = $updated; + + return $this; + } + + public function getDate(): ?\DateTimeImmutable + { + return $this->date; + } + + public function setDate(?\DateTimeImmutable $date): static + { + $this->date = $date; + + return $this; + } +} diff --git a/src/Entity/Tld.php b/src/Entity/Tld.php index c39f127..21ab3dc 100644 --- a/src/Entity/Tld.php +++ b/src/Entity/Tld.php @@ -115,12 +115,15 @@ class Tld public function getTld(): ?string { - return '' === $this->tld ? '.' : $this->tld; + return $this->tld; } public function setTld(string $tld): static { $this->tld = RDAPService::convertToIdn($tld); + if ('' === $this->tld) { + $this->tld = '.'; + } return $this; } diff --git a/src/MessageHandler/UpdateRdapServersHandler.php b/src/MessageHandler/UpdateRdapServersHandler.php index 5ccc811..d73b2f7 100644 --- a/src/MessageHandler/UpdateRdapServersHandler.php +++ b/src/MessageHandler/UpdateRdapServersHandler.php @@ -65,6 +65,12 @@ final readonly class UpdateRdapServersHandler $throws[] = $throwable; } + try { + $this->RDAPService->updateRegistrarListIANA(); + } catch (\Throwable $throwable) { + $throws[] = $throwable; + } + if (!empty($throws)) { throw $throws[0]; } diff --git a/src/Service/RDAPService.php b/src/Service/RDAPService.php index f9deb11..d6b684d 100644 --- a/src/Service/RDAPService.php +++ b/src/Service/RDAPService.php @@ -5,6 +5,7 @@ namespace App\Service; use App\Config\DnsKey\Algorithm; use App\Config\DnsKey\DigestType; use App\Config\EventAction; +use App\Config\RegistrarStatus; use App\Config\TldType; use App\Entity\DnsKey; use App\Entity\Domain; @@ -94,11 +95,6 @@ readonly class RDAPService '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, private EntityRepository $entityRepository, private DomainRepository $domainRepository, @@ -551,24 +547,31 @@ readonly class RDAPService $entity = $this->entityRepository->findOneBy([ 'handle' => $rdapEntity['handle'], - 'tld' => is_numeric($rdapEntity['handle']) ? null : $tld, + 'tld' => $tld, ]); 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.', [ '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']) && null === $entity->getIanaAccreditation()->getStatus()) { $entity->setRemarks($rdapEntity['remarks']); } - if (isset($rdapEntity['vcardArray']) && !in_array($rdapEntity['handle'], self::IANA_RESERVED_IDS)) { + if (isset($rdapEntity['vcardArray']) && null === $entity->getIanaAccreditation()->getStatus()) { if (empty($entity->getJCard())) { if (!array_key_exists('elements', $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']) || null !== $entity->getIanaAccreditation()->getStatus()) { return $entity; } @@ -704,22 +707,26 @@ readonly class RDAPService { foreach ($dnsRoot['services'] as $service) { foreach ($service[0] as $tld) { - if ('' === $tld && null === $this->tldRepository->findOneBy(['tld' => $tld])) { - $this->em->persist((new Tld())->setTld('')->setType(TldType::root)); + if ('.' === $tld && null === $this->tldRepository->findOneBy(['tld' => $tld])) { + $this->em->persist((new Tld())->setTld('.')->setType(TldType::root)); $this->em->flush(); } - $tldReference = $this->em->getReference(Tld::class, $tld); + $tldEntity = $this->tldRepository->findOneBy(['tld' => $tld]); + if (null === $tldEntity) { + $tldEntity = (new Tld())->setTld($tld)->setType(TldType::gTLD); + $this->em->persist($tldEntity); + } 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) { $server = new RdapServer(); } $server - ->setTld($tldReference) + ->setTld($tldEntity) ->setUrl($rdapServerUrl) ->setUpdatedAt(new \DateTimeImmutable($dnsRoot['publication'] ?? 'now')); @@ -792,6 +799,45 @@ readonly class RDAPService $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 (null === $entity) { + $entity = new Entity(); + } + $entity + ->setHandle($registrar->value) + ->setTld(null) + ->setJCard(['vcard', [['version', [], 'text', '4.0'], ['fn', [], 'text', $registrar->name]]]) + ->setRemarks(null) + ->getIanaAccreditation() + ->setRegistrarName($registrar->name) + ->setStatus(RegistrarStatus::from($registrar->status)) + ->setRdapBaseUrl($registrar->rdapurl->count() ? ($registrar->rdapurl->server) : null) + ->setUpdated(null !== $registrar->attributes()->updated ? new \DateTimeImmutable($registrar->attributes()->updated) : null) + ->setDate(null !== $registrar->attributes()->date ? new \DateTimeImmutable($registrar->attributes()->date) : null); + + $this->em->persist($entity); + } + $this->em->flush(); + } + private function getTldType(string $tld): ?TldType { if (in_array(strtolower($tld), self::ISO_TLD_EXCEPTION)) { @@ -835,7 +881,7 @@ readonly class RDAPService if (null === $gtTldEntity) { $gtTldEntity = new Tld(); - $gtTldEntity->setTld($gTld['gTLD']); + $gtTldEntity->setTld($gTld['gTLD'])->setType(TldType::gTLD); $this->logger->notice('New gTLD detected according to ICANN ({tld}).', [ 'tld' => $gTld['gTLD'], ]);