domain-watchdog/src/Service/RDAPService.php

401 lines
14 KiB
PHP
Raw Normal View History

2024-07-13 23:57:07 +02:00
<?php
namespace App\Service;
use App\Config\EventAction;
2024-07-24 18:52:19 +02:00
use App\Config\TldType;
2024-07-13 23:57:07 +02:00
use App\Entity\Domain;
use App\Entity\DomainEntity;
use App\Entity\DomainEvent;
use App\Entity\Entity;
use App\Entity\EntityEvent;
use App\Entity\Nameserver;
use App\Entity\NameserverEntity;
2024-07-18 19:13:06 +02:00
use App\Entity\RdapServer;
2024-07-19 18:59:21 +02:00
use App\Entity\Tld;
2024-07-13 23:57:07 +02:00
use App\Repository\DomainEntityRepository;
use App\Repository\DomainEventRepository;
use App\Repository\DomainRepository;
use App\Repository\EntityEventRepository;
use App\Repository\EntityRepository;
use App\Repository\NameserverEntityRepository;
use App\Repository\NameserverRepository;
2024-07-18 19:13:06 +02:00
use App\Repository\RdapServerRepository;
2024-07-19 18:59:21 +02:00
use App\Repository\TldRepository;
2024-07-13 23:57:07 +02:00
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
2024-07-19 18:59:21 +02:00
use Doctrine\ORM\Exception\ORMException;
2024-07-13 23:57:07 +02:00
use Exception;
2024-07-19 01:17:33 +02:00
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
2024-07-13 23:57:07 +02:00
use Symfony\Contracts\HttpClient\HttpClientInterface;
2024-07-14 11:20:04 +02:00
use Throwable;
2024-07-13 23:57:07 +02:00
2024-07-14 21:15:13 +02:00
readonly class RDAPService
2024-07-13 23:57:07 +02:00
{
2024-07-24 18:52:19 +02:00
const ISO_TLD_EXCEPTION = ['ac', 'eu', 'uk', 'su', 'tp'];
const INFRA_TLD = ['arpa'];
const SPONSORED_TLD = ['edu', 'gov', 'int', 'mil'];
const TEST_TLD = [
'xn--kgbechtv',
'xn--hgbk6aj7f53bba',
'xn--0zwm56d',
'xn--g6w251d',
'xn--80akhbyknj4f',
'xn--11b5bs3a9aj6g',
'xn--jxalpdlp',
'xn--9t4b11yi5a',
'xn--deba0ad',
'xn--zckzah',
'xn--hlcj6aya9esc7a'
];
2024-07-13 23:57:07 +02:00
2024-07-14 21:15:13 +02:00
public function __construct(private HttpClientInterface $client,
private EntityRepository $entityRepository,
private DomainRepository $domainRepository,
private DomainEventRepository $domainEventRepository,
private NameserverRepository $nameserverRepository,
private NameserverEntityRepository $nameserverEntityRepository,
private EntityEventRepository $entityEventRepository,
private DomainEntityRepository $domainEntityRepository,
2024-07-18 19:13:06 +02:00
private RdapServerRepository $rdapServerRepository,
2024-07-19 18:59:21 +02:00
private TldRepository $tldRepository,
2024-07-18 19:13:06 +02:00
private EntityManagerInterface $em
2024-07-13 23:57:07 +02:00
)
{
}
2024-07-14 11:20:04 +02:00
/**
* @throws Exception
*/
public function registerDomains(array $domains): void
{
foreach ($domains as $fqdn) {
2024-07-18 19:13:06 +02:00
$this->registerDomain($fqdn);
}
}
/**
* @throws Exception
*/
public function registerDomain(string $fqdn): Domain
2024-07-13 23:57:07 +02:00
{
2024-07-14 00:05:01 +02:00
$idnDomain = idn_to_ascii($fqdn);
2024-07-19 18:59:21 +02:00
$tld = $this->getTld($idnDomain);
2024-07-18 19:13:06 +02:00
/** @var RdapServer|null $rdapServer */
2024-07-19 18:59:21 +02:00
$rdapServer = $this->rdapServerRepository->findOneBy(["tld" => $tld], ["updatedAt" => "DESC"]);
2024-07-18 19:13:06 +02:00
if ($rdapServer === null) throw new Exception("Unable to determine which RDAP server to contact");
2024-07-13 23:57:07 +02:00
2024-07-14 11:20:04 +02:00
try {
$res = $this->client->request(
2024-07-18 19:13:06 +02:00
'GET', $rdapServer->getUrl() . 'domain/' . $idnDomain
2024-07-14 11:20:04 +02:00
)->toArray();
2024-07-14 21:15:13 +02:00
} catch (Throwable) {
2024-07-14 11:20:04 +02:00
throw new Exception("Unable to contact RDAP server");
}
2024-07-13 23:57:07 +02:00
$domain = $this->domainRepository->findOneBy(["ldhName" => strtolower($res['ldhName'])]);
if ($domain === null) $domain = new Domain();
2024-07-23 14:30:21 +02:00
$domain->setTld($tld)->setLdhName($res['ldhName']);
2024-07-13 23:57:07 +02:00
2024-07-23 14:30:21 +02:00
if (array_key_exists('status', $res)) $domain->setStatus($res['status']);
if (array_key_exists('handle', $res)) $domain->setHandle($res['handle']);
$this->em->persist($domain);
$this->em->flush();
2024-07-13 23:57:07 +02:00
foreach ($res['events'] as $rdapEvent) {
2024-07-23 03:05:35 +02:00
if ($rdapEvent['eventAction'] === EventAction::LastUpdateOfRDAPDatabase->value) continue;
2024-07-13 23:57:07 +02:00
$event = $this->domainEventRepository->findOneBy([
2024-07-23 03:05:35 +02:00
"action" => $rdapEvent['eventAction'],
2024-07-13 23:57:07 +02:00
"date" => new DateTimeImmutable($rdapEvent["eventDate"]),
"domain" => $domain
]);
if ($event === null) $event = new DomainEvent();
$domain->addEvent($event
2024-07-23 03:05:35 +02:00
->setAction($rdapEvent['eventAction'])
2024-07-13 23:57:07 +02:00
->setDate(new DateTimeImmutable($rdapEvent['eventDate'])));
}
2024-07-23 03:13:51 +02:00
if (array_key_exists('entities', $res) && is_array($res['entities'])) {
2024-07-23 03:13:51 +02:00
foreach ($res['entities'] as $rdapEntity) {
if (!array_key_exists('handle', $rdapEntity) || $rdapEntity['handle'] === '') continue;
2024-07-23 03:13:51 +02:00
2024-07-14 00:06:58 +02:00
$entity = $this->registerEntity($rdapEntity);
2024-07-13 23:57:07 +02:00
$this->em->persist($entity);
$this->em->flush();
2024-07-23 03:13:51 +02:00
$domainEntity = $this->domainEntityRepository->findOneBy([
"domain" => $domain,
2024-07-13 23:57:07 +02:00
"entity" => $entity
]);
2024-07-23 03:13:51 +02:00
if ($domainEntity === null) $domainEntity = new DomainEntity();
$roles = array_map(
fn($e) => $e['roles'],
array_filter(
$res['entities'],
fn($e) => array_key_exists('handle', $e) && $e['handle'] === $rdapEntity['handle']
)
);
2024-07-23 03:13:51 +02:00
if (count($roles) !== count($roles, COUNT_RECURSIVE)) $roles = array_merge(...$roles);
2024-07-13 23:57:07 +02:00
2024-07-23 03:13:51 +02:00
$domain->addDomainEntity($domainEntity
->setDomain($domain)
2024-07-13 23:57:07 +02:00
->setEntity($entity)
2024-07-23 03:05:35 +02:00
->setRoles($roles));
2024-07-23 03:13:51 +02:00
$this->em->persist($domainEntity);
$this->em->flush();
2024-07-13 23:57:07 +02:00
}
2024-07-23 03:13:51 +02:00
}
2024-07-13 23:57:07 +02:00
2024-07-23 03:13:51 +02:00
if (array_key_exists('nameservers', $res) && is_array($res['nameservers'])) {
foreach ($res['nameservers'] as $rdapNameserver) {
$nameserver = $this->nameserverRepository->findOneBy([
"ldhName" => strtolower($rdapNameserver['ldhName'])
]);
if ($nameserver === null) $nameserver = new Nameserver();
$nameserver->setLdhName($rdapNameserver['ldhName']);
if (!array_key_exists('entities', $rdapNameserver) || !is_array($rdapNameserver['entities'])) {
$domain->addNameserver($nameserver);
continue;
}
foreach ($rdapNameserver['entities'] as $rdapEntity) {
if (!array_key_exists('handle', $rdapEntity) || $rdapEntity['handle'] === '') continue;
$entity = $this->registerEntity($rdapEntity);
$this->em->persist($entity);
$this->em->flush();
$nameserverEntity = $this->nameserverEntityRepository->findOneBy([
"nameserver" => $nameserver,
"entity" => $entity
]);
if ($nameserverEntity === null) $nameserverEntity = new NameserverEntity();
$roles = array_merge(
...array_map(
fn(array $e): array => $e['roles'],
array_filter(
$rdapNameserver['entities'],
fn($e) => array_key_exists('handle', $e) && $e['handle'] === $rdapEntity['handle']
)
)
);
$nameserver->addNameserverEntity($nameserverEntity
->setNameserver($nameserver)
->setEntity($entity)
->setStatus($rdapNameserver['status'])
->setRoles($roles));
}
$domain->addNameserver($nameserver);
}
2024-07-13 23:57:07 +02:00
}
$domain->updateTimestamps();
2024-07-13 23:57:07 +02:00
$this->em->persist($domain);
$this->em->flush();
return $domain;
2024-07-13 23:57:07 +02:00
}
2024-07-14 21:15:13 +02:00
/**
* @throws Exception
*/
2024-07-19 18:59:21 +02:00
private function getTld($domain): ?object
2024-07-13 23:57:07 +02:00
{
$lastDotPosition = strrpos($domain, '.');
if ($lastDotPosition === false) {
2024-07-14 11:20:04 +02:00
throw new Exception("Domain must contain at least one dot");
2024-07-13 23:57:07 +02:00
}
2024-07-19 18:59:21 +02:00
$tld = strtolower(substr($domain, $lastDotPosition + 1));
return $this->tldRepository->findOneBy(["tld" => $tld]);
2024-07-13 23:57:07 +02:00
}
2024-07-14 11:20:04 +02:00
/**
* @throws Exception
*/
2024-07-14 00:06:58 +02:00
private function registerEntity(array $rdapEntity): Entity
2024-07-13 23:57:07 +02:00
{
$entity = $this->entityRepository->findOneBy([
"handle" => $rdapEntity['handle']
]);
if ($entity === null) $entity = new Entity();
$entity->setHandle($rdapEntity['handle']);
2024-07-23 03:05:35 +02:00
if (array_key_exists('vcardArray', $rdapEntity)) {
if (empty($entity->getJCard())) {
$entity->setJCard($rdapEntity['vcardArray']);
} else {
$properties = [];
foreach ($rdapEntity['vcardArray'][1] as $prop) {
$properties[$prop[0]] = $prop;
}
foreach ($entity->getJCard()[1] as $prop) {
$properties[$prop[0]] = $prop;
}
$entity->setJCard(["vcard", array_values($properties)]);
2024-07-13 23:57:07 +02:00
}
}
if (!array_key_exists('events', $rdapEntity)) return $entity;
foreach ($rdapEntity['events'] as $rdapEntityEvent) {
$eventAction = $rdapEntityEvent["eventAction"];
if ($eventAction === EventAction::LastChanged->value || $eventAction === EventAction::LastUpdateOfRDAPDatabase->value) continue;
2024-07-13 23:57:07 +02:00
$event = $this->entityEventRepository->findOneBy([
2024-07-23 03:05:35 +02:00
"action" => $rdapEntityEvent["eventAction"],
2024-07-13 23:57:07 +02:00
"date" => new DateTimeImmutable($rdapEntityEvent["eventDate"])
]);
if ($event !== null) continue;
$entity->addEvent(
(new EntityEvent())
->setEntity($entity)
2024-07-23 03:05:35 +02:00
->setAction($rdapEntityEvent["eventAction"])
2024-07-13 23:57:07 +02:00
->setDate(new DateTimeImmutable($rdapEntityEvent['eventDate'])));
}
return $entity;
}
2024-07-18 19:13:06 +02:00
2024-07-19 01:17:33 +02:00
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
2024-07-19 18:59:21 +02:00
* @throws ORMException
2024-07-19 01:17:33 +02:00
*/
2024-07-18 19:13:06 +02:00
public function updateRDAPServers(): void
{
$dnsRoot = $this->client->request(
'GET', 'https://data.iana.org/rdap/dns.json'
)->toArray();
foreach ($dnsRoot['services'] as $service) {
foreach ($service[0] as $tld) {
2024-07-19 20:40:30 +02:00
if ($tld === "") continue;
2024-07-19 18:59:21 +02:00
$tldReference = $this->em->getReference(Tld::class, $tld);
2024-07-18 19:13:06 +02:00
foreach ($service[1] as $rdapServerUrl) {
2024-07-24 18:52:19 +02:00
$server = $this->rdapServerRepository->findOneBy(["tld" => $tldReference, "url" => $rdapServerUrl]);
2024-07-18 19:13:06 +02:00
if ($server === null) $server = new RdapServer();
2024-07-19 18:59:21 +02:00
$server->setTld($tldReference)->setUrl($rdapServerUrl)->updateTimestamps();
2024-07-18 19:13:06 +02:00
$this->em->persist($server);
}
}
}
$this->em->flush();
}
2024-07-19 18:59:21 +02:00
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function updateTldListIANA(): void
{
$tldList = array_map(
fn($tld) => strtolower($tld),
explode(PHP_EOL,
$this->client->request(
'GET', 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt'
)->getContent()
));
array_shift($tldList);
2024-07-24 18:52:19 +02:00
foreach ($tldList as $tld) {
2024-07-19 20:40:30 +02:00
if ($tld === "") continue;
2024-07-24 18:52:19 +02:00
$tldEntity = $this->tldRepository->findOneBy(['tld' => $tld]);
if ($tldEntity === null) $tldEntity = new Tld();
2024-07-24 21:58:45 +02:00
if ($tldEntity->getType() === null) {
$type = $this->getTldType($tld);
if ($type !== null) {
$tldEntity->setType($type);
} elseif ($tldEntity->isContractTerminated() === null) {
$tldEntity->setType(TldType::ccTLD);
} else {
$tldEntity->setType(TldType::gTLD);
}
}
2024-07-24 18:52:19 +02:00
$this->em->persist($tldEntity);
2024-07-19 18:59:21 +02:00
}
$this->em->flush();
}
2024-07-24 18:52:19 +02:00
private function getTldType(string $tld): ?TldType
{
2024-07-24 21:58:45 +02:00
if (in_array($tld, self::ISO_TLD_EXCEPTION)) return TldType::ccTLD;
2024-07-24 18:52:19 +02:00
if (in_array(strtolower($tld), self::INFRA_TLD)) return TldType::iTLD;
if (in_array(strtolower($tld), self::SPONSORED_TLD)) return TldType::sTLD;
if (in_array(strtolower($tld), self::TEST_TLD)) return TldType::tTLD;
2024-07-24 21:58:45 +02:00
return null;
2024-07-24 18:52:19 +02:00
}
2024-07-19 18:59:21 +02:00
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
* @throws DecodingExceptionInterface
* @throws Exception
2024-07-24 18:52:19 +02:00
* @throws ORMException
2024-07-19 18:59:21 +02:00
*/
public function updateGTldListICANN(): void
{
$gTldList = $this->client->request(
'GET', 'https://www.icann.org/resources/registries/gtlds/v2/gtlds.json'
)->toArray()['gTLDs'];
foreach ($gTldList as $gTld) {
2024-07-19 20:40:30 +02:00
if ($gTld['gTLD'] === "") continue;
2024-07-24 18:52:19 +02:00
/** @var Tld $gtTldEntity */
$gtTldEntity = $this->em->getReference(Tld::class, $gTld['gTLD']);
2024-07-19 18:59:21 +02:00
2024-07-24 21:58:45 +02:00
$gtTldEntity
->setContractTerminated($gTld['contractTerminated'])
2024-07-19 18:59:21 +02:00
->setRegistryOperator($gTld['registryOperator'])
->setSpecification13($gTld['specification13']);
2024-07-24 18:52:19 +02:00
2024-07-19 18:59:21 +02:00
if ($gTld['removalDate'] !== null) $gtTldEntity->setRemovalDate(new DateTimeImmutable($gTld['removalDate']));
if ($gTld['delegationDate'] !== null) $gtTldEntity->setDelegationDate(new DateTimeImmutable($gTld['delegationDate']));
if ($gTld['dateOfContractSignature'] !== null) $gtTldEntity->setDateOfContractSignature(new DateTimeImmutable($gTld['dateOfContractSignature']));
$this->em->persist($gtTldEntity);
}
$this->em->flush();
}
2024-07-13 23:57:07 +02:00
}