diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index ca8a7b3..17ffa87 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -9,6 +9,14 @@ framework: retry_strategy: max_retries: 3 multiplier: 2 + rdap_async: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + retry_strategy: + delay: 1000 + multiplier: 2 + max_retries: 10 + max_delay: 10000 + failed: 'doctrine://default?queue_name=failed' # sync: 'sync://' @@ -23,11 +31,12 @@ framework: Symfony\Component\Notifier\Message\SmsMessage: async App\Message\OrderDomain: async - App\Message\ProcessWatchlistTrigger: async - App\Message\SendDomainEventNotif: async - App\Message\UpdateDomainsFromWatchlist: async + App\Message\DetectDomainChange: async + App\Message\ProcessAllWatchlist: async + App\Message\ProcessWatchlist: async App\Message\UpdateRdapServers: async App\Message\ValidateConnectorCredentials: async + App\Message\UpdateDomain: async # Route your messages to the transports # 'App\Message\YourMessage': async diff --git a/config/packages/rate_limiter.yaml b/config/packages/rate_limiter.yaml index 11e6dc1..b93e8c5 100644 --- a/config/packages/rate_limiter.yaml +++ b/config/packages/rate_limiter.yaml @@ -20,7 +20,12 @@ framework: limit: 5 rate: { interval: '5 minutes' } - rdap_requests: + user_rdap_requests: policy: sliding_window limit: 10 - interval: '1 hour' \ No newline at end of file + interval: '1 hour' + + rdap_requests: + policy: sliding_window + limit: 2 + interval: '1 second' diff --git a/src/Command/ProcessWatchlistCommand.php b/src/Command/ProcessWatchlistCommand.php index 1d4a84a..a923f13 100644 --- a/src/Command/ProcessWatchlistCommand.php +++ b/src/Command/ProcessWatchlistCommand.php @@ -2,7 +2,7 @@ namespace App\Command; -use App\Message\ProcessWatchlistTrigger; +use App\Message\ProcessAllWatchlist; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -33,7 +33,7 @@ class ProcessWatchlistCommand extends Command { $io = new SymfonyStyle($input, $output); - $this->bus->dispatch(new ProcessWatchlistTrigger()); + $this->bus->dispatch(new ProcessAllWatchlist()); $io->success('Watchlist processing triggered!'); diff --git a/src/Command/RegisterDomainCommand.php b/src/Command/RegisterDomainCommand.php index 7d4e281..9a611d1 100644 --- a/src/Command/RegisterDomainCommand.php +++ b/src/Command/RegisterDomainCommand.php @@ -3,7 +3,7 @@ namespace App\Command; use App\Entity\Watchlist; -use App\Message\SendDomainEventNotif; +use App\Message\DetectDomainChange; use App\Repository\DomainRepository; use App\Service\RDAPService; use Random\Randomizer; @@ -64,7 +64,7 @@ class RegisterDomainCommand extends Command /** @var Watchlist $watchlist */ foreach ($watchlists as $watchlist) { - $this->bus->dispatch(new SendDomainEventNotif($watchlist->getToken(), $domain->getLdhName(), $updatedAt)); + $this->bus->dispatch(new DetectDomainChange($watchlist->getToken(), $domain->getLdhName(), $updatedAt)); } } } catch (\Throwable $e) { diff --git a/src/Controller/MeController.php b/src/Controller/MeController.php index 20ca66e..4590eb2 100644 --- a/src/Controller/MeController.php +++ b/src/Controller/MeController.php @@ -12,14 +12,14 @@ class MeController extends AbstractController { public function __construct( private readonly SerializerInterface $serializer, - private readonly RateLimiterFactory $rdapRequestsLimiter, + private readonly RateLimiterFactory $userRdapRequestsLimiter, ) { } public function __invoke(): Response { $user = $this->getUser(); - $limiter = $this->rdapRequestsLimiter->create($user->getUserIdentifier()); + $limiter = $this->userRdapRequestsLimiter->create($user->getUserIdentifier()); $limit = $limiter->consume(0); $data = $this->serializer->serialize($user, 'json', ['groups' => 'user:list']); diff --git a/src/Message/SendDomainEventNotif.php b/src/Message/DetectDomainChange.php similarity index 85% rename from src/Message/SendDomainEventNotif.php rename to src/Message/DetectDomainChange.php index 9866617..dfe6831 100644 --- a/src/Message/SendDomainEventNotif.php +++ b/src/Message/DetectDomainChange.php @@ -2,7 +2,7 @@ namespace App\Message; -final class SendDomainEventNotif +final class DetectDomainChange { public function __construct( public string $watchlistToken, diff --git a/src/Message/ProcessWatchlistTrigger.php b/src/Message/ProcessAllWatchlist.php similarity index 87% rename from src/Message/ProcessWatchlistTrigger.php rename to src/Message/ProcessAllWatchlist.php index 4e44f16..bf405b9 100644 --- a/src/Message/ProcessWatchlistTrigger.php +++ b/src/Message/ProcessAllWatchlist.php @@ -2,7 +2,7 @@ namespace App\Message; -final class ProcessWatchlistTrigger +final class ProcessAllWatchlist { /* * Add whatever properties and methods you need diff --git a/src/Message/UpdateDomainsFromWatchlist.php b/src/Message/ProcessWatchlist.php similarity index 71% rename from src/Message/UpdateDomainsFromWatchlist.php rename to src/Message/ProcessWatchlist.php index 41bed74..136f593 100644 --- a/src/Message/UpdateDomainsFromWatchlist.php +++ b/src/Message/ProcessWatchlist.php @@ -2,7 +2,7 @@ namespace App\Message; -final readonly class UpdateDomainsFromWatchlist +final readonly class ProcessWatchlist { public function __construct( public string $watchlistToken, diff --git a/src/Message/UpdateDomain.php b/src/Message/UpdateDomain.php new file mode 100644 index 0000000..758b1ce --- /dev/null +++ b/src/Message/UpdateDomain.php @@ -0,0 +1,15 @@ +watchlistRepository->findOneBy(['token' => $message->watchlistToken]); diff --git a/src/MessageHandler/ProcessWatchlistTriggerHandler.php b/src/MessageHandler/ProcessAllWatchlistHandler.php similarity index 71% rename from src/MessageHandler/ProcessWatchlistTriggerHandler.php rename to src/MessageHandler/ProcessAllWatchlistHandler.php index 52010ad..64a96a9 100644 --- a/src/MessageHandler/ProcessWatchlistTriggerHandler.php +++ b/src/MessageHandler/ProcessAllWatchlistHandler.php @@ -3,8 +3,8 @@ namespace App\MessageHandler; use App\Entity\Watchlist; -use App\Message\ProcessWatchlistTrigger; -use App\Message\UpdateDomainsFromWatchlist; +use App\Message\ProcessAllWatchlist; +use App\Message\ProcessWatchlist; use App\Repository\WatchlistRepository; use Random\Randomizer; use Symfony\Component\Messenger\Attribute\AsMessageHandler; @@ -12,7 +12,7 @@ use Symfony\Component\Messenger\Exception\ExceptionInterface; use Symfony\Component\Messenger\MessageBusInterface; #[AsMessageHandler] -final readonly class ProcessWatchlistTriggerHandler +final readonly class ProcessAllWatchlistHandler { public function __construct( private WatchlistRepository $watchlistRepository, @@ -23,11 +23,13 @@ final readonly class ProcessWatchlistTriggerHandler /** * @throws ExceptionInterface */ - public function __invoke(ProcessWatchlistTrigger $message): void + public function __invoke(ProcessAllWatchlist $message): void { /* * We shuffle the watch lists to process them in an order that we consider random. * The shuffling is provided by a high-level API of a CSPRNG number generator. + * + * ProcessAllWatchlist -> ProcessWatchlist -> UpdateDomain -> (DetectDomainChange & OrderDomain) */ $randomizer = new Randomizer(); @@ -35,7 +37,7 @@ final readonly class ProcessWatchlistTriggerHandler /** @var Watchlist $watchlist */ foreach ($watchlists as $watchlist) { - $this->bus->dispatch(new UpdateDomainsFromWatchlist($watchlist->getToken())); + $this->bus->dispatch(new ProcessWatchlist($watchlist->getToken())); } } } diff --git a/src/MessageHandler/UpdateDomainsFromWatchlistHandler.php b/src/MessageHandler/ProcessWatchlistHandler.php similarity index 55% rename from src/MessageHandler/UpdateDomainsFromWatchlistHandler.php rename to src/MessageHandler/ProcessWatchlistHandler.php index c0aa1e8..3a14848 100644 --- a/src/MessageHandler/UpdateDomainsFromWatchlistHandler.php +++ b/src/MessageHandler/ProcessWatchlistHandler.php @@ -4,53 +4,36 @@ namespace App\MessageHandler; use App\Entity\Domain; use App\Entity\Watchlist; -use App\Exception\DomainNotFoundException; -use App\Exception\TldNotSupportedException; -use App\Exception\UnknownRdapServerException; use App\Message\OrderDomain; -use App\Message\SendDomainEventNotif; -use App\Message\UpdateDomainsFromWatchlist; -use App\Notifier\DomainDeletedNotification; -use App\Repository\DomainRepository; +use App\Message\ProcessWatchlist; +use App\Message\UpdateDomain; use App\Repository\WatchlistRepository; -use App\Service\ChatNotificationService; use App\Service\Provider\AbstractProvider; use App\Service\Provider\CheckDomainProviderInterface; use App\Service\RDAPService; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\MessageBusInterface; -use Symfony\Component\Mime\Address; -use Symfony\Component\Notifier\Recipient\Recipient; #[AsMessageHandler] -final readonly class UpdateDomainsFromWatchlistHandler +final readonly class ProcessWatchlistHandler { - private Address $sender; - public function __construct( private RDAPService $RDAPService, - private ChatNotificationService $chatNotificationService, - private MailerInterface $mailer, - string $mailerSenderEmail, - string $mailerSenderName, private MessageBusInterface $bus, private WatchlistRepository $watchlistRepository, private LoggerInterface $logger, #[Autowire(service: 'service_container')] private ContainerInterface $locator, - private DomainRepository $domainRepository, ) { - $this->sender = new Address($mailerSenderEmail, $mailerSenderName); } /** * @throws \Throwable */ - public function __invoke(UpdateDomainsFromWatchlist $message): void + public function __invoke(ProcessWatchlist $message): void { /** @var Watchlist $watchlist */ $watchlist = $this->watchlistRepository->findOneBy(['token' => $message->watchlistToken]); @@ -98,37 +81,7 @@ final readonly class UpdateDomainsFromWatchlistHandler /** @var Domain $domain */ foreach ($watchlist->getDomains()->filter(fn ($domain) => $this->RDAPService->isToBeUpdated($domain, false, null !== $watchlist->getConnector())) as $domain ) { - $updatedAt = $domain->getUpdatedAt(); - $deleted = $domain->getDeleted(); - - try { - /* - * Domain name update - * We send messages that correspond to the sending of notifications that will not be processed here. - */ - $this->RDAPService->registerDomain($domain->getLdhName()); - $this->bus->dispatch(new SendDomainEventNotif($watchlist->getToken(), $domain->getLdhName(), $updatedAt)); - } catch (DomainNotFoundException) { - $newDomain = $this->domainRepository->findOneBy(['ldhName' => $domain->getLdhName()]); - - if (!$deleted && null !== $newDomain && $newDomain->getDeleted()) { - $notification = new DomainDeletedNotification($this->sender, $domain); - $this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage()); - $this->chatNotificationService->sendChatNotification($watchlist, $notification); - } - - if ($watchlist->getConnector()) { - /* - * If the domain name no longer appears in the WHOIS AND a connector is associated with this Watchlist, - * this connector is used to purchase the domain name. - */ - $this->bus->dispatch(new OrderDomain($watchlist->getToken(), $domain->getLdhName())); - } - } catch (TldNotSupportedException|UnknownRdapServerException) { - /* - * In this case, the domain name can no longer be updated. Unfortunately, there is nothing more that can be done. - */ - } + $this->bus->dispatch(new UpdateDomain($domain->getLdhName(), $watchlist->getToken())); } } diff --git a/src/MessageHandler/UpdateDomainHandler.php b/src/MessageHandler/UpdateDomainHandler.php new file mode 100644 index 0000000..720a4be --- /dev/null +++ b/src/MessageHandler/UpdateDomainHandler.php @@ -0,0 +1,127 @@ +sender = new Address($mailerSenderEmail, $mailerSenderName); + } + + /** + * @throws TransportExceptionInterface + * @throws \Symfony\Component\Notifier\Exception\TransportExceptionInterface + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + * @throws OptimisticLockException + * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface + * @throws UnsupportedDsnSchemeException + * @throws ServerExceptionInterface + * @throws MalformedDomainException + * @throws ExceptionInterface + */ + public function __invoke(UpdateDomain $message): void + { + $domain = $this->domainRepository->findOneBy(['ldhName' => $message->ldhName]); + /** @var ?RdapServer $rdapServer */ + $rdapServer = $domain->getTld()->getRdapServers()->first(); + if (null === $rdapServer) { + $this->logger->warning('No RDAP server for this domain name', [ + 'ldhName' => $domain->getLdhName(), + ]); + + return; + } + $limiter = $this->rdapRequestsLimiter->create($rdapServer->getUrl()); + $limit = $limiter->consume(); + + if (!$limit->isAccepted()) { + $retryAfter = $limit->getRetryAfter()->getTimestamp() - time(); + + $this->logger->warning('Security rate limit reached for this RDAP server', [ + 'url' => $rdapServer->getUrl(), + 'retryAfter' => $retryAfter, + ]); + + throw new RecoverableMessageHandlingException('Rate limit reached', 0, null, $retryAfter); + } + + $watchlist = $this->watchlistRepository->findOneBy(['token' => $message->watchlistToken]); + + $updatedAt = $domain->getUpdatedAt(); + $deleted = $domain->getDeleted(); + + try { + /* + * Domain name update + * We send messages that correspond to the sending of notifications that will not be processed here. + */ + $this->RDAPService->registerDomain($domain->getLdhName()); + $this->bus->dispatch(new DetectDomainChange($watchlist->getToken(), $domain->getLdhName(), $updatedAt)); + } catch (DomainNotFoundException) { + $newDomain = $this->domainRepository->findOneBy(['ldhName' => $domain->getLdhName()]); + + if (!$deleted && null !== $newDomain && $newDomain->getDeleted()) { + $notification = new DomainDeletedNotification($this->sender, $domain); + $this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage()); + $this->chatNotificationService->sendChatNotification($watchlist, $notification); + } + + if ($watchlist->getConnector()) { + /* + * If the domain name no longer appears in the WHOIS AND a connector is associated with this Watchlist, + * this connector is used to purchase the domain name. + */ + $this->bus->dispatch(new OrderDomain($watchlist->getToken(), $domain->getLdhName())); + } + } catch (TldNotSupportedException|UnknownRdapServerException) { + /* + * In this case, the domain name can no longer be updated. Unfortunately, there is nothing more that can be done. + */ + } + } +} diff --git a/src/Scheduler/SendNotifWatchlistTriggerSchedule.php b/src/Scheduler/ProcessWatchlistSchedule.php similarity index 76% rename from src/Scheduler/SendNotifWatchlistTriggerSchedule.php rename to src/Scheduler/ProcessWatchlistSchedule.php index bad174e..0487f5c 100644 --- a/src/Scheduler/SendNotifWatchlistTriggerSchedule.php +++ b/src/Scheduler/ProcessWatchlistSchedule.php @@ -2,15 +2,15 @@ namespace App\Scheduler; -use App\Message\ProcessWatchlistTrigger; +use App\Message\ProcessAllWatchlist; use Symfony\Component\Scheduler\Attribute\AsSchedule; use Symfony\Component\Scheduler\RecurringMessage; use Symfony\Component\Scheduler\Schedule; use Symfony\Component\Scheduler\ScheduleProviderInterface; use Symfony\Contracts\Cache\CacheInterface; -#[AsSchedule('notif_watchlist')] -final readonly class SendNotifWatchlistTriggerSchedule implements ScheduleProviderInterface +#[AsSchedule('process_watchlist')] +final readonly class ProcessWatchlistSchedule implements ScheduleProviderInterface { public function __construct( private CacheInterface $cache, @@ -21,7 +21,7 @@ final readonly class SendNotifWatchlistTriggerSchedule implements ScheduleProvid { return (new Schedule()) ->add( - RecurringMessage::every('5 minutes', new ProcessWatchlistTrigger()), + RecurringMessage::every('5 minutes', new ProcessAllWatchlist()), ) ->stateful($this->cache); } diff --git a/src/State/AutoRegisterDomainProvider.php b/src/State/AutoRegisterDomainProvider.php index e640526..6089563 100644 --- a/src/State/AutoRegisterDomainProvider.php +++ b/src/State/AutoRegisterDomainProvider.php @@ -10,7 +10,7 @@ use App\Exception\DomainNotFoundException; use App\Exception\MalformedDomainException; use App\Exception\TldNotSupportedException; use App\Exception\UnknownRdapServerException; -use App\Message\SendDomainEventNotif; +use App\Message\DetectDomainChange; use App\Repository\DomainRepository; use App\Service\RDAPService; use Doctrine\ORM\EntityManagerInterface; @@ -37,7 +37,7 @@ readonly class AutoRegisterDomainProvider implements ProviderInterface private RDAPService $RDAPService, private KernelInterface $kernel, private ParameterBagInterface $parameterBag, - private RateLimiterFactory $rdapRequestsLimiter, + private RateLimiterFactory $userRdapRequestsLimiter, private Security $security, private LoggerInterface $logger, private DomainRepository $domainRepository, @@ -92,7 +92,7 @@ readonly class AutoRegisterDomainProvider implements ProviderInterface } if (false === $this->kernel->isDebug() && true === $this->parameterBag->get('limited_features')) { - $limiter = $this->rdapRequestsLimiter->create($userId); + $limiter = $this->userRdapRequestsLimiter->create($userId); $limit = $limiter->consume(); if (!$limit->isAccepted()) { @@ -131,7 +131,7 @@ readonly class AutoRegisterDomainProvider implements ProviderInterface /** @var Watchlist $watchlist */ foreach ($watchlists as $watchlist) { - $this->bus->dispatch(new SendDomainEventNotif($watchlist->getToken(), $domain->getLdhName(), $updatedAt)); + $this->bus->dispatch(new DetectDomainChange($watchlist->getToken(), $domain->getLdhName(), $updatedAt)); } return $domain; diff --git a/tests/MessageHandler/ProcessWatchlistTriggerHandlerTest.php b/tests/MessageHandler/ProcessWatchlistTriggerHandlerTest.php index 91d45cb..341becd 100644 --- a/tests/MessageHandler/ProcessWatchlistTriggerHandlerTest.php +++ b/tests/MessageHandler/ProcessWatchlistTriggerHandlerTest.php @@ -2,9 +2,9 @@ namespace App\Tests\MessageHandler; -use App\Message\ProcessWatchlistTrigger; -use App\Message\UpdateDomainsFromWatchlist; -use App\MessageHandler\ProcessWatchlistTriggerHandler; +use App\Message\ProcessAllWatchlist; +use App\Message\ProcessWatchlist; +use App\MessageHandler\ProcessAllWatchlistHandler; use App\Tests\State\WatchlistUpdateProcessorTest; use PHPUnit\Framework\Attributes\DependsExternal; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -16,16 +16,16 @@ final class ProcessWatchlistTriggerHandlerTest extends KernelTestCase public function testSendMessage() { $container = self::getContainer(); - $handler = $container->get(ProcessWatchlistTriggerHandler::class); + $handler = $container->get(ProcessAllWatchlistHandler::class); /** @var TraceableMessageBus $bus */ $bus = $container->get('messenger.bus.default'); $bus->reset(); - $handler(new ProcessWatchlistTrigger()); + $handler(new ProcessAllWatchlist()); $dispatched = $bus->getDispatchedMessages(); $this->assertNotEmpty($dispatched); - $this->assertInstanceOf(UpdateDomainsFromWatchlist::class, $dispatched[0]['message']); + $this->assertInstanceOf(ProcessWatchlist::class, $dispatched[0]['message']); } } diff --git a/tests/MessageHandler/UpdateDomainsFromWatchlistHandlerTest.php b/tests/MessageHandler/UpdateDomainsFromWatchlistHandlerTest.php index 3a345db..7c3a7be 100644 --- a/tests/MessageHandler/UpdateDomainsFromWatchlistHandlerTest.php +++ b/tests/MessageHandler/UpdateDomainsFromWatchlistHandlerTest.php @@ -4,8 +4,8 @@ namespace App\Tests\MessageHandler; use App\Entity\Domain; use App\Entity\Watchlist; -use App\Message\UpdateDomainsFromWatchlist; -use App\MessageHandler\UpdateDomainsFromWatchlistHandler; +use App\Message\ProcessWatchlist; +use App\MessageHandler\ProcessWatchlistHandler; use App\Tests\State\WatchlistUpdateProcessorTest; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\Attributes\DependsExternal; @@ -20,7 +20,7 @@ final class UpdateDomainsFromWatchlistHandlerTest extends KernelTestCase { $container = self::getContainer(); $entityManager = $container->get(EntityManagerInterface::class); - $handler = $container->get(UpdateDomainsFromWatchlistHandler::class); + $handler = $container->get(ProcessWatchlistHandler::class); $bus = $container->get('messenger.bus.default'); $deletedDomainLdhName = new UuidV4().'.com'; @@ -40,7 +40,7 @@ final class UpdateDomainsFromWatchlistHandlerTest extends KernelTestCase /* @var TraceableMessageBus $bus */ $bus->reset(); - $handler(new UpdateDomainsFromWatchlist($watchlist->getToken())); + $handler(new ProcessWatchlist($watchlist->getToken())); $this->expectNotToPerformAssertions(); }