feat: add search for a tld via RDAP

This commit is contained in:
Maël Gangloff 2024-12-20 17:43:35 +01:00
parent 7c606e2697
commit 1dc16d769b
No known key found for this signature in database
GPG Key ID: 11FDC81C24A7F629
9 changed files with 86 additions and 21 deletions

View File

@ -12,16 +12,22 @@ export function DomainDiagram({domain}: { domain: Domain }) {
useEffect(() => { useEffect(() => {
const e = getLayoutedElements([ const nodes = [
domainToNode(domain), domainToNode(domain),
...domainEntitiesToNode(domain, true), ...domainEntitiesToNode(domain, true),
tldToNode(domain.tld),
...domain.nameservers.map(nsToNode) ...domain.nameservers.map(nsToNode)
].flat(), [ ].flat()
const edges = [
domainEntitiesToEdges(domain, true), domainEntitiesToEdges(domain, true),
tldToEdge(domain),
...domainNSToEdges(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) setNodes(e.nodes)
setEdges(e.edges) setEdges(e.edges)

View File

@ -23,7 +23,7 @@ export function DomainResult({domain}: { domain: Domain }) {
<Badge.Ribbon text={ <Badge.Ribbon text={
<Tooltip <Tooltip
title={tld.type === 'ccTLD' ? regionNames.of(getCountryCode(tld.tld)) : tld.type === 'gTLD' ? tld?.registryOperator : undefined}> title={tld.type === 'ccTLD' ? regionNames.of(getCountryCode(tld.tld)) : tld.type === 'gTLD' ? tld?.registryOperator : undefined}>
{`.${domain.tld.tld.toUpperCase()} (${tld.type})`} {`${(domain.tld.tld === '.' ? '' : '.') + domain.tld.tld.toUpperCase()} (${tld.type})`}
</Tooltip> </Tooltip>
} }
color={ color={

View File

@ -19,7 +19,7 @@ export function DomainSearchBar({onFinish}: { onFinish: (values: FieldType) => v
required: true, required: true,
message: t`Required` message: t`Required`
}, { }, {
pattern: /^(?=.*\.)\S*[^.\s]$/, pattern: /^(?=.*\.)?\S*[^.\s]$/,
message: t`This domain name does not appear to be valid`, message: t`This domain name does not appear to be valid`,
max: 63, max: 63,
min: 2 min: 2

View File

@ -47,7 +47,7 @@ export function watchlistToNodes(watchlist: Watchlist, withRegistrar = false, wi
const domains = watchlist.domains.map(domainToNode) const domains = watchlist.domains.map(domainToNode)
const entities = [...new Set(watchlist.domains.map(d => domainEntitiesToNode(d, withRegistrar)).flat())] 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) const nameservers = [...new Set(watchlist.domains.map(d => d.nameservers))].flat().map(nsToNode, withRegistrar)
return [...domains, ...entities, ...nameservers, ...(withTld ? tlds : [])] return [...domains, ...entities, ...nameservers, ...(withTld ? tlds : [])]

View File

@ -20,6 +20,7 @@
"publication": "1970-01-01T00:00:00Z", "publication": "1970-01-01T00:00:00Z",
"services": [ "services": [
[ [ "" ], [ "https://rdap.iana.org/" ] ], # If you want to make RDAP queries to get TLD information from IANA
[ [ "ad" ], [ "https://rdap.nic.ad/" ] ], [ [ "ad" ], [ "https://rdap.nic.ad/" ] ],
[ [ "ae" ], [ "https://rdap.nic.ae/" ] ], [ [ "ae" ], [ "https://rdap.nic.ae/" ] ],
[ [ "ki" ], [ "https://rdap.nic.ki/" ] ], [ [ "ki" ], [ "https://rdap.nic.ki/" ] ],

View File

@ -0,0 +1,33 @@
<?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 Version20241220161843 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('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;');
}
}

View File

@ -9,4 +9,5 @@ enum TldType: string
case sTLD = 'sTLD'; case sTLD = 'sTLD';
case ccTLD = 'ccTLD'; case ccTLD = 'ccTLD';
case tTLD = 'tTLD'; case tTLD = 'tTLD';
case root = 'root';
} }

View File

@ -107,7 +107,7 @@ class Tld
public function getTld(): ?string public function getTld(): ?string
{ {
return $this->tld; return '' === $this->tld ? '.' : $this->tld;
} }
public function setTld(string $tld): static public function setTld(string $tld): static
@ -194,8 +194,10 @@ class Tld
return $this->type; return $this->type;
} }
public function setType(?TldType $type): void public function setType(?TldType $type): static
{ {
$this->type = $type; $this->type = $type;
return $this;
} }
} }

View File

@ -191,7 +191,7 @@ readonly class RDAPService
if (count($addedStatus) > 0 || count($deletedStatus) > 0) { if (count($addedStatus) > 0 || count($deletedStatus) > 0) {
$this->em->persist($domain); $this->em->persist($domain);
if ($domain->getUpdatedAt() == $domain->getCreatedAt()) { if ($domain->getUpdatedAt() != $domain->getCreatedAt()) {
$this->em->persist((new DomainStatus()) $this->em->persist((new DomainStatus())
->setDomain($domain) ->setDomain($domain)
->setDate($domain->getUpdatedAt()) ->setDate($domain->getUpdatedAt())
@ -252,10 +252,6 @@ readonly class RDAPService
if (array_key_exists('entities', $res) && is_array($res['entities'])) { if (array_key_exists('entities', $res) && is_array($res['entities'])) {
foreach ($res['entities'] as $rdapEntity) { foreach ($res['entities'] as $rdapEntity) {
if (!array_key_exists('handle', $rdapEntity) || '' === $rdapEntity['handle']) {
continue;
}
$entity = $this->registerEntity($rdapEntity); $entity = $this->registerEntity($rdapEntity);
$this->em->persist($entity); $this->em->persist($entity);
@ -274,10 +270,14 @@ readonly class RDAPService
fn ($e) => $e['roles'], fn ($e) => $e['roles'],
array_filter( array_filter(
$res['entities'], $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 * Flatten the array
*/ */
@ -344,7 +344,7 @@ readonly class RDAPService
fn (array $e): array => $e['roles'], fn (array $e): array => $e['roles'],
array_filter( array_filter(
$rdapNameserver['entities'], $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 private function getTld($domain): ?object
{ {
if (!str_contains($domain, '.')) {
return $this->tldRepository->findOneBy(['tld' => '']);
}
$lastDotPosition = strrpos($domain, '.'); $lastDotPosition = strrpos($domain, '.');
if (false === $lastDotPosition) { if (false === $lastDotPosition) {
throw new BadRequestException('Domain must contain at least one dot'); throw new BadRequestException('Domain must contain at least one dot');
@ -388,6 +391,21 @@ readonly class RDAPService
*/ */
private function registerEntity(array $rdapEntity): Entity 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([ $entity = $this->entityRepository->findOneBy([
'handle' => $rdapEntity['handle'], 'handle' => $rdapEntity['handle'],
]); ]);
@ -481,9 +499,13 @@ 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) { 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); $tldReference = $this->em->getReference(Tld::class, $tld);
foreach ($service[1] as $rdapServerUrl) { foreach ($service[1] as $rdapServerUrl) {
$server = $this->rdapServerRepository->findOneBy(['tld' => $tldReference, 'url' => $rdapServerUrl]); $server = $this->rdapServerRepository->findOneBy(['tld' => $tldReference, 'url' => $rdapServerUrl]);
if (null === $server) { if (null === $server) {
@ -601,10 +623,10 @@ readonly class RDAPService
if ('' === $gTld['gTLD']) { if ('' === $gTld['gTLD']) {
continue; continue;
} }
/** @var Tld $gtTldEntity */ /** @var Tld|null $gtTldEntity */
$gtTldEntity = $this->tldRepository->findOneBy(['tld' => $gTld['gTLD']]); $gtTldEntity = $this->tldRepository->findOneBy(['tld' => $gTld['gTLD']]);
if (null == $gtTldEntity) { if (null === $gtTldEntity) {
$gtTldEntity = new Tld(); $gtTldEntity = new Tld();
$gtTldEntity->setTld($gTld['gTLD']); $gtTldEntity->setTld($gTld['gTLD']);
$this->logger->notice('New gTLD detected according to ICANN ({tld}).', [ $this->logger->notice('New gTLD detected according to ICANN ({tld}).', [