mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-29 16:15:04 +00:00
feat: also use registrar RDAP server
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
"protonlabs/vobject": "^4.31",
|
||||
"psr/http-client": "^1.0",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"scienta/doctrine-json-functions": "^6.3",
|
||||
"symfony/asset": "7.3.*",
|
||||
"symfony/asset-mapper": "7.3.*",
|
||||
"symfony/cache": "7.3.*",
|
||||
|
||||
76
composer.lock
generated
76
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "49fd7fad6776160ff572f3fb9201a8a2",
|
||||
"content-hash": "1db291e0d108c6bb06d447134f1250c6",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/core",
|
||||
@@ -4189,6 +4189,78 @@
|
||||
},
|
||||
"time": "2024-09-06T08:00:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "scienta/doctrine-json-functions",
|
||||
"version": "6.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ScientaNL/DoctrineJsonFunctions.git",
|
||||
"reference": "554b2fd281e976a791501fc4753ffd4c5891ec62"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ScientaNL/DoctrineJsonFunctions/zipball/554b2fd281e976a791501fc4753ffd4c5891ec62",
|
||||
"reference": "554b2fd281e976a791501fc4753ffd4c5891ec62",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/dbal": "^3.2 || ^4",
|
||||
"doctrine/lexer": "^2.0 || ^3.0",
|
||||
"doctrine/orm": "^2.19 || ^3",
|
||||
"ext-pdo": "*",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^9.0 || ^10.0 || ^11.0 || ^12.0",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpstan/phpstan-doctrine": "^1.4",
|
||||
"phpstan/phpstan-phpunit": "^1.4",
|
||||
"phpunit/phpunit": "^10.1",
|
||||
"psalm/plugin-phpunit": "^0.18",
|
||||
"slevomat/coding-standard": "~8",
|
||||
"symfony/cache": "^5.4 || ^6.4 || ^7",
|
||||
"vimeo/psalm": "^5.2",
|
||||
"webmozart/assert": "^1.11"
|
||||
},
|
||||
"suggest": {
|
||||
"dunglas/doctrine-json-odm": "To serialize / deserialize objects as JSON documents."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Scienta\\DoctrineJsonFunctions\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Doctrine Json Functions Contributors",
|
||||
"homepage": "https://github.com/ScientaNL/DoctrineJsonFunctions/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A set of extensions to Doctrine that add support for json query functions.",
|
||||
"keywords": [
|
||||
"database",
|
||||
"doctrine",
|
||||
"dql",
|
||||
"json",
|
||||
"mariadb",
|
||||
"mysql",
|
||||
"orm",
|
||||
"postgres",
|
||||
"postgresql",
|
||||
"sqlite"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/ScientaNL/DoctrineJsonFunctions/issues",
|
||||
"source": "https://github.com/ScientaNL/DoctrineJsonFunctions/tree/6.3.0"
|
||||
},
|
||||
"time": "2024-11-08T12:33:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/asset",
|
||||
"version": "v7.3.0",
|
||||
@@ -15319,7 +15391,7 @@
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": ">=8.2",
|
||||
"php": ">=8.4",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-simplexml": "*"
|
||||
|
||||
@@ -24,6 +24,9 @@ doctrine:
|
||||
alias: App
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
dql:
|
||||
string_functions:
|
||||
JSONB_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Postgresql\JsonbContains
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
|
||||
39
migrations/Version20251023000555.php
Normal file
39
migrations/Version20251023000555.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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 Version20251023000555 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('DROP INDEX uniq_e28446850f7084e918020d9');
|
||||
$this->addSql('ALTER TABLE entity ADD from_accredited_registrar_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE entity ADD CONSTRAINT FK_E2844687CB19E6A FOREIGN KEY (from_accredited_registrar_id) REFERENCES icann_accreditation (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_E2844687CB19E6A ON entity (from_accredited_registrar_id)');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_E28446850F7084E918020D97CB19E6A ON entity (tld_id, handle, from_accredited_registrar_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE entity DROP CONSTRAINT FK_E2844687CB19E6A');
|
||||
$this->addSql('DROP INDEX IDX_E2844687CB19E6A');
|
||||
$this->addSql('DROP INDEX UNIQ_E28446850F7084E918020D97CB19E6A');
|
||||
$this->addSql('ALTER TABLE entity DROP from_accredited_registrar_id');
|
||||
$this->addSql('CREATE UNIQUE INDEX uniq_e28446850f7084e918020d9 ON entity (tld_id, handle)');
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
|
||||
#[ORM\Entity(repositoryClass: EntityRepository::class)]
|
||||
#[ORM\UniqueConstraint(
|
||||
columns: ['tld_id', 'handle']
|
||||
columns: ['tld_id', 'handle', 'from_accredited_registrar_id']
|
||||
)]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
@@ -96,6 +96,9 @@ class Entity
|
||||
#[Groups(['entity:list', 'entity:item', 'domain:item'])]
|
||||
private ?IcannAccreditation $icannAccreditation = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
private ?IcannAccreditation $fromAccreditedRegistrar = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->domainEntities = new ArrayCollection();
|
||||
@@ -264,4 +267,16 @@ class Entity
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFromAccreditedRegistrar(): ?IcannAccreditation
|
||||
{
|
||||
return $this->fromAccreditedRegistrar;
|
||||
}
|
||||
|
||||
public function setFromAccreditedRegistrar(?IcannAccreditation $fromAccreditedRegistrar): static
|
||||
{
|
||||
$this->fromAccreditedRegistrar = $fromAccreditedRegistrar;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,20 @@ class DomainEntityRepository extends ServiceEntityRepository
|
||||
->getQuery()->execute();
|
||||
}
|
||||
|
||||
public function getDomainEntityFromDomainAndRoles(Domain $domain, array $roles)
|
||||
{
|
||||
return $this->createQueryBuilder('de')
|
||||
->select()
|
||||
->where('de.deletedAt IS NULL')
|
||||
->andWhere('de.domain = :domain')
|
||||
->andWhere('JSONB_CONTAINS(de.roles, :roles) = true')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->setParameter('domain', $domain)
|
||||
->setParameter('roles', json_encode($roles))
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return DomainEntity[] Returns an array of DomainEntity objects
|
||||
// */
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Entity\DomainEvent;
|
||||
use App\Entity\DomainStatus;
|
||||
use App\Entity\Entity;
|
||||
use App\Entity\EntityEvent;
|
||||
use App\Entity\IcannAccreditation;
|
||||
use App\Entity\Nameserver;
|
||||
use App\Entity\NameserverEntity;
|
||||
use App\Entity\RdapServer;
|
||||
@@ -49,6 +50,7 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
class RDAPService
|
||||
{
|
||||
public const ENTITY_HANDLE_BLACKLIST = [
|
||||
'REDACTED',
|
||||
'REDACTED_FOR_PRIVACY',
|
||||
'ANO00-FRNIC',
|
||||
'not applicable',
|
||||
@@ -117,7 +119,7 @@ class RDAPService
|
||||
* @throws UnknownRdapServerException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function registerDomain(string $fqdn): Domain
|
||||
public function registerDomain(string $fqdn, ?IcannAccreditation $icannAccreditation = null): Domain
|
||||
{
|
||||
$idnDomain = RDAPService::convertToIdn($fqdn);
|
||||
$tld = $this->getTld($idnDomain);
|
||||
@@ -126,10 +128,11 @@ class RDAPService
|
||||
'ldhName' => $idnDomain,
|
||||
]);
|
||||
|
||||
$rdapServer = $this->fetchRdapServer($tld);
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $idnDomain]);
|
||||
|
||||
$rdapData = $this->fetchRdapResponse($rdapServer, $idnDomain, $domain);
|
||||
$rdapServer = $icannAccreditation ? (new RdapServer())->setUrl($icannAccreditation->getRdapBaseUrl()) : $this->fetchRdapServer($tld);
|
||||
|
||||
$rdapData = $this->fetchRdapResponse($rdapServer, $idnDomain, $domain, null === $icannAccreditation);
|
||||
$this->em->beginTransaction();
|
||||
|
||||
if (null === $domain) {
|
||||
@@ -139,25 +142,49 @@ class RDAPService
|
||||
|
||||
$this->em->lock($domain, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
$this->updateDomainStatus($domain, $rdapData);
|
||||
if (null === $icannAccreditation) {
|
||||
$this->updateDomainStatus($domain, $rdapData);
|
||||
|
||||
if (in_array('free', $domain->getStatus())) {
|
||||
throw DomainNotFoundException::fromDomain($idnDomain);
|
||||
if (in_array('free', $domain->getStatus())) {
|
||||
throw DomainNotFoundException::fromDomain($idnDomain);
|
||||
}
|
||||
|
||||
$domain
|
||||
->setRdapServer($rdapServer)
|
||||
->setDelegationSigned(isset($rdapData['secureDNS']['delegationSigned']) && $rdapData['secureDNS']['delegationSigned']);
|
||||
|
||||
$this->updateDomainHandle($domain, $rdapData);
|
||||
|
||||
$this->updateDomainEvents($domain, $rdapData);
|
||||
|
||||
$this->updateDomainEntities($domain, $rdapData);
|
||||
|
||||
$this->updateDomainNameservers($domain, $rdapData);
|
||||
$this->updateDomainDsData($domain, $rdapData);
|
||||
|
||||
$domain->setDeleted(false)->updateTimestamps();
|
||||
|
||||
/** @var ?DomainEntity $registrar */
|
||||
$registrar = $this->domainEntityRepository->getDomainEntityFromDomainAndRoles($domain, ['registrar']);
|
||||
|
||||
if (null !== $registrar?->getEntity()?->getIcannAccreditation()?->getRdapBaseUrl()) {
|
||||
try {
|
||||
$this->registerDomain($idnDomain, $registrar->getEntity()->getIcannAccreditation());
|
||||
} catch (DomainNotFoundException) {
|
||||
$this->logger->warning('Domain name exists for the registry but not for the registrar', [
|
||||
'ldhName' => $domain->getLdhName(),
|
||||
'icannAccreditation' => $registrar->getEntity()->getIcannAccreditation()->getId(),
|
||||
]);
|
||||
} catch (\Throwable) {
|
||||
$this->logger->error('Domain name cannot be updated from the registrar RDAP server', [
|
||||
'ldhName' => $domain->getLdhName(),
|
||||
'icannAccreditation' => $registrar->getEntity()->getIcannAccreditation()->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->updateDomainEntities($domain, $rdapData, $icannAccreditation);
|
||||
}
|
||||
|
||||
$domain
|
||||
->setRdapServer($rdapServer)
|
||||
->setDelegationSigned(isset($rdapData['secureDNS']['delegationSigned']) && $rdapData['secureDNS']['delegationSigned']);
|
||||
|
||||
$this->updateDomainHandle($domain, $rdapData);
|
||||
|
||||
$this->updateDomainEvents($domain, $rdapData);
|
||||
$this->updateDomainEntities($domain, $rdapData);
|
||||
$this->updateDomainNameservers($domain, $rdapData);
|
||||
$this->updateDomainDsData($domain, $rdapData);
|
||||
|
||||
$domain->setDeleted(false)->updateTimestamps();
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->commit();
|
||||
|
||||
@@ -240,7 +267,7 @@ class RDAPService
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function fetchRdapResponse(RdapServer $rdapServer, string $idnDomain, ?Domain $domain): array
|
||||
private function fetchRdapResponse(RdapServer $rdapServer, string $idnDomain, ?Domain $domain, bool $handleRdapException = true): array
|
||||
{
|
||||
$rdapServerUrl = $rdapServer->getUrl();
|
||||
$this->logger->info('An RDAP query to update this domain name will be made', [
|
||||
@@ -254,7 +281,10 @@ class RDAPService
|
||||
|
||||
return $req->toArray();
|
||||
} catch (\Exception $e) {
|
||||
throw $this->handleRdapException($e, $idnDomain, $domain, $req ?? null);
|
||||
if ($handleRdapException) {
|
||||
throw $this->handleRdapException($e, $idnDomain, $domain, $req ?? null);
|
||||
}
|
||||
throw DomainNotFoundException::fromDomain($idnDomain);
|
||||
} finally {
|
||||
if ($this->influxdbEnabled && isset($req)) {
|
||||
$this->influxService->addRdapQueryPoint($rdapServer, $idnDomain, $req->getInfo());
|
||||
@@ -392,9 +422,11 @@ class RDAPService
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function updateDomainEntities(Domain $domain, array $rdapData): void
|
||||
private function updateDomainEntities(Domain $domain, array $rdapData, ?IcannAccreditation $icannAccreditation = null): void
|
||||
{
|
||||
$this->domainEntityRepository->setDomainEntityAsDeleted($domain);
|
||||
if (!$icannAccreditation) {
|
||||
$this->domainEntityRepository->setDomainEntityAsDeleted($domain);
|
||||
}
|
||||
|
||||
if (!isset($rdapData['entities']) || !is_array($rdapData['entities'])) {
|
||||
return;
|
||||
@@ -402,7 +434,11 @@ class RDAPService
|
||||
|
||||
foreach ($rdapData['entities'] as $rdapEntity) {
|
||||
$roles = $this->extractEntityRoles($rdapData['entities'], $rdapEntity);
|
||||
$entity = $this->registerEntity($rdapEntity, $roles, $domain->getLdhName(), $domain->getTld());
|
||||
if ($domain->getDomainEntities()->findFirst(fn (int $i, DomainEntity $de) => count(array_intersect($de->getRoles(), $roles)) > 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entity = $this->registerEntity($rdapEntity, $roles, $domain->getLdhName(), $domain->getTld(), $icannAccreditation);
|
||||
|
||||
$domainEntity = $this->domainEntityRepository->findOneBy([
|
||||
'domain' => $domain,
|
||||
@@ -513,8 +549,8 @@ class RDAPService
|
||||
? $targetEntity['handle'] === $e['handle']
|
||||
: (
|
||||
isset($targetEntity['vcardArray']) && isset($e['vcardArray'])
|
||||
? $targetEntity['vcardArray'] === $e['vcardArray']
|
||||
: $targetEntity === $e
|
||||
? $targetEntity['vcardArray'] === $e['vcardArray']
|
||||
: $targetEntity === $e
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -529,7 +565,7 @@ class RDAPService
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function registerEntity(array $rdapEntity, array $roles, string $domain, Tld $tld): Entity
|
||||
private function registerEntity(array $rdapEntity, array $roles, string $domain, Tld $tld, ?IcannAccreditation $fromAccreditedRegistrar = null): Entity
|
||||
{
|
||||
/*
|
||||
* If there is no number to identify the entity, one is generated from the domain name and the roles associated with this entity
|
||||
@@ -547,6 +583,7 @@ class RDAPService
|
||||
$entity = $this->entityRepository->findOneBy([
|
||||
'handle' => $rdapEntity['handle'],
|
||||
'tld' => $tld,
|
||||
'fromAccreditedRegistrar' => $fromAccreditedRegistrar,
|
||||
]);
|
||||
|
||||
if (null === $entity) {
|
||||
@@ -574,7 +611,7 @@ class RDAPService
|
||||
}
|
||||
}
|
||||
|
||||
$entity->setHandle($rdapEntity['handle'])->setIcannAccreditation($icannAccreditation);
|
||||
$entity->setHandle($rdapEntity['handle'])->setIcannAccreditation($icannAccreditation)->setFromAccreditedRegistrar($fromAccreditedRegistrar);
|
||||
|
||||
if (isset($rdapEntity['remarks']) && is_array($rdapEntity['remarks'])) {
|
||||
$entity->setRemarks($rdapEntity['remarks']);
|
||||
|
||||
Reference in New Issue
Block a user