diff --git a/migrations/Version20250812002458.php b/migrations/Version20250812002458.php new file mode 100644 index 0000000..5d05d71 --- /dev/null +++ b/migrations/Version20250812002458.php @@ -0,0 +1,35 @@ +addSql('CREATE TABLE dns_key (algorithm INT NOT NULL, digest_type INT NOT NULL, key_tag BYTEA NOT NULL, digest BYTEA NOT NULL, domain_id VARCHAR(255) NOT NULL, PRIMARY KEY(algorithm, digest_type, key_tag, domain_id, digest))'); + $this->addSql('CREATE INDEX IDX_88A62EF2115F0EE5 ON dns_key (domain_id)'); + $this->addSql('ALTER TABLE dns_key ADD CONSTRAINT FK_88A62EF2115F0EE5 FOREIGN KEY (domain_id) REFERENCES domain (ldh_name) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + 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 dns_key DROP CONSTRAINT FK_88A62EF2115F0EE5'); + $this->addSql('DROP TABLE dns_key'); + } +} diff --git a/src/Config/DnsKey/Algorithm.php b/src/Config/DnsKey/Algorithm.php new file mode 100644 index 0000000..c62d04a --- /dev/null +++ b/src/Config/DnsKey/Algorithm.php @@ -0,0 +1,36 @@ +algorithm; + } + + public function setAlgorithm(?Algorithm $algorithm): static + { + $this->algorithm = $algorithm; + + return $this; + } + + public function getDigestType(): ?DigestType + { + return $this->digestType; + } + + public function setDigestType(DigestType $digestType): static + { + $this->digestType = $digestType; + + return $this; + } + + public function getKeyTag() + { + return unpack('n', $this->keyTag)[1]; + } + + public function setKeyTag($keyTag): static + { + $this->keyTag = $keyTag; + + return $this; + } + + public function getDomain(): ?Domain + { + return $this->domain; + } + + public function setDomain(?Domain $domain): static + { + $this->domain = $domain; + + return $this; + } + + public function getDigest() + { + return strtoupper(bin2hex($this->digest)); + } + + public function setDigest($digest): static + { + $this->digest = $digest; + + return $this; + } +} diff --git a/src/Entity/Domain.php b/src/Entity/Domain.php index 41bc25e..3b15241 100644 --- a/src/Entity/Domain.php +++ b/src/Entity/Domain.php @@ -51,6 +51,7 @@ use Symfony\Component\Serializer\Attribute\SerializedName; 'nameserver-entity:nameserver', 'nameserver-entity:entity', 'tld:item', + 'ds:list', ], ], read: false @@ -136,6 +137,13 @@ class Domain #[Groups(['domain:item', 'domain:list'])] private ?bool $delegationSigned = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: DnsKey::class, mappedBy: 'domain', orphanRemoval: true)] + #[Groups(['domain:item'])] + private Collection $dnsKey; + private const IMPORTANT_EVENTS = [EventAction::Deletion->value, EventAction::Expiration->value]; private const IMPORTANT_STATUS = [ 'redemption period', @@ -158,6 +166,7 @@ class Domain $this->createdAt = $this->updatedAt; $this->deleted = false; $this->domainStatuses = new ArrayCollection(); + $this->dnsKey = new ArrayCollection(); } public function getLdhName(): ?string @@ -607,6 +616,36 @@ class Domain ]); } + /** + * @return Collection + */ + public function getDnsKey(): Collection + { + return $this->dnsKey; + } + + public function addDnsKey(DnsKey $dnsKey): static + { + if (!$this->dnsKey->contains($dnsKey)) { + $this->dnsKey->add($dnsKey); + $dnsKey->setDomain($this); + } + + return $this; + } + + public function removeDnsKey(DnsKey $dnsKey): static + { + if ($this->dnsKey->removeElement($dnsKey)) { + // set the owning side to null (unless already changed) + if ($dnsKey->getDomain() === $this) { + $dnsKey->setDomain(null); + } + } + + return $this; + } + /** * @return Event[] * diff --git a/src/Repository/DnsKeyRepository.php b/src/Repository/DnsKeyRepository.php new file mode 100644 index 0000000..481f92e --- /dev/null +++ b/src/Repository/DnsKeyRepository.php @@ -0,0 +1,43 @@ + + */ +class DnsKeyRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, DnsKey::class); + } + + // /** + // * @return DnsKey[] Returns an array of DnsKey objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('d') + // ->andWhere('d.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('d.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?DnsKey + // { + // return $this->createQueryBuilder('d') + // ->andWhere('d.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Service/RDAPService.php b/src/Service/RDAPService.php index c4d8288..4e19c2e 100644 --- a/src/Service/RDAPService.php +++ b/src/Service/RDAPService.php @@ -2,8 +2,11 @@ namespace App\Service; +use App\Config\DnsKey\Algorithm; +use App\Config\DnsKey\DigestType; use App\Config\EventAction; use App\Config\TldType; +use App\Entity\DnsKey; use App\Entity\Domain; use App\Entity\DomainEntity; use App\Entity\DomainEvent; @@ -31,6 +34,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpClient\Exception\ClientException; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; use Symfony\Component\Yaml\Yaml; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; @@ -171,6 +175,7 @@ readonly class RDAPService $this->updateDomainEvents($domain, $rdapData); $this->updateDomainEntities($domain, $rdapData); $this->updateDomainNameservers($domain, $rdapData); + $this->updateDomainDsData($domain, $rdapData); $domain->setDeleted(false)->updateTimestamps(); @@ -268,7 +273,18 @@ readonly class RDAPService 'idnDomain' => $idnDomain, ]); - $domain->setDeleted(true)->updateTimestamps(); + $domain->updateTimestamps(); + + if (!$domain->getDeleted() && $domain->getUpdatedAt() !== $domain->getCreatedAt()) { + $this->em->persist((new DomainStatus()) + ->setDomain($domain) + ->setCreatedAt($domain->getUpdatedAt()) + ->setDate($domain->getUpdatedAt()) + ->setAddStatus([]) + ->setDeleteStatus($domain->getStatus())); + } + + $domain->setDeleted(true); $this->em->persist($domain); $this->em->flush(); } @@ -625,6 +641,42 @@ readonly class RDAPService return $entity; } + private function updateDomainDsData(Domain $domain, array $rdapData): void + { + $domain->getDnsKey()->clear(); + $this->em->persist($domain); + $this->em->flush(); + + if (array_key_exists('secureDNS', $rdapData) && array_key_exists('dsData', $rdapData['secureDNS']) && is_array($rdapData['secureDNS']['dsData'])) { + foreach ($rdapData['secureDNS']['dsData'] as $rdapDsData) { + $dsData = new DnsKey(); + if (array_key_exists('keyTag', $rdapDsData)) { + $dsData->setKeyTag(pack('n', $rdapDsData['keyTag'])); + } + if (array_key_exists('algorithm', $rdapDsData)) { + $dsData->setAlgorithm(Algorithm::from($rdapDsData['algorithm'])); + } + if (array_key_exists('digest', $rdapDsData)) { + $blob = hex2bin($rdapDsData['digest']); + if (false === $blob) { + throw new ServiceUnavailableHttpException('DNSSEC digest is not a valid hexadecimal value.'); + } + $dsData->setDigest($blob); + } + if (array_key_exists('digestType', $rdapDsData)) { + $dsData->setDigestType(DigestType::from($rdapDsData['digestType'])); + } + + $domain->addDnsKey($dsData); + $this->em->persist($dsData); + } + } else { + $this->logger->warning('The domain name {idnDomain} has no DS record.', [ + 'idnDomain' => $domain->getLdhName(), + ]); + } + } + /** * @throws TransportExceptionInterface * @throws ServerExceptionInterface