From 1dc16d769bfb0b805f3f37e0848ce2f70bdc5718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Gangloff?= Date: Fri, 20 Dec 2024 17:43:35 +0100 Subject: [PATCH] feat: add search for a tld via RDAP --- assets/components/search/DomainDiagram.tsx | 16 ++++--- assets/components/search/DomainResult.tsx | 2 +- assets/components/search/DomainSearchBar.tsx | 2 +- .../watchlist/diagram/watchlistToNodes.tsx | 2 +- config/app/custom_rdap_servers.example.yaml | 3 +- migrations/Version20241220161843.php | 33 +++++++++++++++ src/Config/TldType.php | 1 + src/Entity/Tld.php | 6 ++- src/Service/RDAPService.php | 42 ++++++++++++++----- 9 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 migrations/Version20241220161843.php diff --git a/assets/components/search/DomainDiagram.tsx b/assets/components/search/DomainDiagram.tsx index fe2d009..d52da76 100644 --- a/assets/components/search/DomainDiagram.tsx +++ b/assets/components/search/DomainDiagram.tsx @@ -12,16 +12,22 @@ export function DomainDiagram({domain}: { domain: Domain }) { useEffect(() => { - const e = getLayoutedElements([ + const nodes = [ domainToNode(domain), ...domainEntitiesToNode(domain, true), - tldToNode(domain.tld), ...domain.nameservers.map(nsToNode) - ].flat(), [ + ].flat() + const edges = [ domainEntitiesToEdges(domain, true), - tldToEdge(domain), ...domainNSToEdges(domain) - ].flat()) + ].flat() + + if (domain.tld.tld !== '.') { + nodes.push(tldToNode(domain.tld)) + edges.push(tldToEdge(domain)) + } + + const e = getLayoutedElements(nodes, edges) setNodes(e.nodes) setEdges(e.edges) diff --git a/assets/components/search/DomainResult.tsx b/assets/components/search/DomainResult.tsx index ed4d1db..5464f7d 100644 --- a/assets/components/search/DomainResult.tsx +++ b/assets/components/search/DomainResult.tsx @@ -23,7 +23,7 @@ export function DomainResult({domain}: { domain: Domain }) { - {`.${domain.tld.tld.toUpperCase()} (${tld.type})`} + {`${(domain.tld.tld === '.' ? '' : '.') + domain.tld.tld.toUpperCase()} (${tld.type})`} } color={ diff --git a/assets/components/search/DomainSearchBar.tsx b/assets/components/search/DomainSearchBar.tsx index 3926136..b712fe1 100644 --- a/assets/components/search/DomainSearchBar.tsx +++ b/assets/components/search/DomainSearchBar.tsx @@ -19,7 +19,7 @@ export function DomainSearchBar({onFinish}: { onFinish: (values: FieldType) => v required: true, message: t`Required` }, { - pattern: /^(?=.*\.)\S*[^.\s]$/, + pattern: /^(?=.*\.)?\S*[^.\s]$/, message: t`This domain name does not appear to be valid`, max: 63, min: 2 diff --git a/assets/components/tracking/watchlist/diagram/watchlistToNodes.tsx b/assets/components/tracking/watchlist/diagram/watchlistToNodes.tsx index 49e58a5..967328c 100644 --- a/assets/components/tracking/watchlist/diagram/watchlistToNodes.tsx +++ b/assets/components/tracking/watchlist/diagram/watchlistToNodes.tsx @@ -47,7 +47,7 @@ export function watchlistToNodes(watchlist: Watchlist, withRegistrar = false, wi const domains = watchlist.domains.map(domainToNode) const entities = [...new Set(watchlist.domains.map(d => domainEntitiesToNode(d, withRegistrar)).flat())] - const tlds = [...new Set(watchlist.domains.map(d => d.tld))].map(tldToNode) + const tlds = [...new Set(watchlist.domains.map(d => d.tld))].filter(t => t.tld !== '.').map(tldToNode) const nameservers = [...new Set(watchlist.domains.map(d => d.nameservers))].flat().map(nsToNode, withRegistrar) return [...domains, ...entities, ...nameservers, ...(withTld ? tlds : [])] diff --git a/config/app/custom_rdap_servers.example.yaml b/config/app/custom_rdap_servers.example.yaml index 7953247..7688aaf 100644 --- a/config/app/custom_rdap_servers.example.yaml +++ b/config/app/custom_rdap_servers.example.yaml @@ -20,7 +20,8 @@ "publication": "1970-01-01T00:00:00Z", "services": [ - [ [ "ad" ], [ "https://rdap.nic.ad/" ] ], + [ [ "" ], [ "https://rdap.iana.org/" ] ], # If you want to make RDAP queries to get TLD information from IANA + [ [ "ad" ], [ "https://rdap.nic.ad/" ] ], [ [ "ae" ], [ "https://rdap.nic.ae/" ] ], [ [ "ki" ], [ "https://rdap.nic.ki/" ] ], [ [ "af" ], [ "https://rdap.nic.af/" ] ], diff --git a/migrations/Version20241220161843.php b/migrations/Version20241220161843.php new file mode 100644 index 0000000..b4369f0 --- /dev/null +++ b/migrations/Version20241220161843.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE entity ALTER COLUMN j_card TYPE JSONB USING j_card::JSONB;'); + $this->addSql('ALTER TABLE connector ALTER COLUMN auth_data TYPE JSONB USING auth_data::JSONB;'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE entity ALTER COLUMN j_card TYPE JSON USING j_card::JSON;'); + $this->addSql('ALTER TABLE connector ALTER COLUMN auth_data TYPE JSON USING auth_data::JSON;'); + } +} diff --git a/src/Config/TldType.php b/src/Config/TldType.php index 363eb2c..0a7e830 100644 --- a/src/Config/TldType.php +++ b/src/Config/TldType.php @@ -9,4 +9,5 @@ enum TldType: string case sTLD = 'sTLD'; case ccTLD = 'ccTLD'; case tTLD = 'tTLD'; + case root = 'root'; } diff --git a/src/Entity/Tld.php b/src/Entity/Tld.php index 1b141ce..cdce3e0 100644 --- a/src/Entity/Tld.php +++ b/src/Entity/Tld.php @@ -107,7 +107,7 @@ class Tld public function getTld(): ?string { - return $this->tld; + return '' === $this->tld ? '.' : $this->tld; } public function setTld(string $tld): static @@ -194,8 +194,10 @@ class Tld return $this->type; } - public function setType(?TldType $type): void + public function setType(?TldType $type): static { $this->type = $type; + + return $this; } } diff --git a/src/Service/RDAPService.php b/src/Service/RDAPService.php index 18da204..7571ed0 100644 --- a/src/Service/RDAPService.php +++ b/src/Service/RDAPService.php @@ -191,7 +191,7 @@ readonly class RDAPService if (count($addedStatus) > 0 || count($deletedStatus) > 0) { $this->em->persist($domain); - if ($domain->getUpdatedAt() == $domain->getCreatedAt()) { + if ($domain->getUpdatedAt() != $domain->getCreatedAt()) { $this->em->persist((new DomainStatus()) ->setDomain($domain) ->setDate($domain->getUpdatedAt()) @@ -252,10 +252,6 @@ readonly class RDAPService if (array_key_exists('entities', $res) && is_array($res['entities'])) { foreach ($res['entities'] as $rdapEntity) { - if (!array_key_exists('handle', $rdapEntity) || '' === $rdapEntity['handle']) { - continue; - } - $entity = $this->registerEntity($rdapEntity); $this->em->persist($entity); @@ -274,10 +270,14 @@ readonly class RDAPService fn ($e) => $e['roles'], array_filter( $res['entities'], - fn ($e) => array_key_exists('handle', $e) && $e['handle'] === $rdapEntity['handle'] + fn ($e) => $rdapEntity === $e ) ); + if (0 == count($roles)) { + dd($entity); + } + /* * Flatten the array */ @@ -344,7 +344,7 @@ readonly class RDAPService fn (array $e): array => $e['roles'], array_filter( $rdapNameserver['entities'], - fn ($e) => array_key_exists('handle', $e) && $e['handle'] === $rdapEntity['handle'] + fn ($e) => $rdapEntity === $e ) ) ); @@ -374,6 +374,9 @@ readonly class RDAPService private function getTld($domain): ?object { + if (!str_contains($domain, '.')) { + return $this->tldRepository->findOneBy(['tld' => '']); + } $lastDotPosition = strrpos($domain, '.'); if (false === $lastDotPosition) { throw new BadRequestException('Domain must contain at least one dot'); @@ -388,6 +391,21 @@ readonly class RDAPService */ private function registerEntity(array $rdapEntity): Entity { + $conn = $this->em->getConnection(); + $sql = 'SELECT * FROM entity WHERE j_Card @> :data'; + $stmt = $conn->prepare($sql)->executeQuery(['data' => json_encode($rdapEntity['vcardArray'])]); + + $result = $stmt->fetchAllAssociative(); + + $rdapEntity['handle'] = array_key_exists('handle', $rdapEntity) && '' !== $rdapEntity['handle'] + ? $rdapEntity['handle'] + : ( + count($result) > 0 + ? $result[0]['handle'] + : 'DW-NOHANDLE-'.hash('md5', json_encode($rdapEntity) + ) + ); + $entity = $this->entityRepository->findOneBy([ 'handle' => $rdapEntity['handle'], ]); @@ -481,9 +499,13 @@ readonly class RDAPService foreach ($dnsRoot['services'] as $service) { foreach ($service[0] as $tld) { if ('' === $tld) { - continue; + if (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); + foreach ($service[1] as $rdapServerUrl) { $server = $this->rdapServerRepository->findOneBy(['tld' => $tldReference, 'url' => $rdapServerUrl]); if (null === $server) { @@ -601,10 +623,10 @@ readonly class RDAPService if ('' === $gTld['gTLD']) { continue; } - /** @var Tld $gtTldEntity */ + /** @var Tld|null $gtTldEntity */ $gtTldEntity = $this->tldRepository->findOneBy(['tld' => $gTld['gTLD']]); - if (null == $gtTldEntity) { + if (null === $gtTldEntity) { $gtTldEntity = new Tld(); $gtTldEntity->setTld($gTld['gTLD']); $this->logger->notice('New gTLD detected according to ICANN ({tld}).', [