src/Controller/Controller.php line 94

  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Controller;
  4. use App\Dto\WlSettingsDto;
  5. use App\Enum\MirrorStatusEnum;
  6. use App\Exception\RotatorNotFoundException;
  7. use App\Message\CallbackLog;
  8. use App\Service\MirrorService;
  9. use App\Service\PlayerTokenService;
  10. use App\Service\RotatorService;
  11. use App\Service\WlRotatorDomainService;
  12. use App\Service\WlSettingsService;
  13. use DateTimeImmutable;
  14. use Doctrine\DBAL\Exception as DbalException;
  15. use InfluxDB\Database\Exception as InfluxDatabaseException;
  16. use InfluxDB\Exception as InfluxException;
  17. use JsonException;
  18. use MarfaTech\Bundle\MetricBundle\Client\InfluxDb\InfluxDbClient;
  19. use MarfaTech\Component\IdGenerator\Generator\IdGenerator;
  20. use Psr\Log\LoggerAwareInterface;
  21. use Psr\Log\LoggerAwareTrait;
  22. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  23. use Symfony\Component\HttpFoundation\Cookie;
  24. use Symfony\Component\HttpFoundation\JsonResponse;
  25. use Symfony\Component\HttpFoundation\RedirectResponse;
  26. use Symfony\Component\HttpFoundation\Request;
  27. use Symfony\Component\HttpFoundation\Response;
  28. use Symfony\Component\OptionsResolver\Exception\ExceptionInterface;
  29. use Symfony\Component\Routing\Annotation\Route;
  30. use Throwable;
  31. use App\Message\ApiRequestLog;
  32. use Symfony\Component\Messenger\MessageBusInterface;
  33. use function array_merge;
  34. use function base64_decode;
  35. use function http_build_query;
  36. use function is_string;
  37. use function json_decode;
  38. use function json_encode;
  39. use function sha1;
  40. use function sprintf;
  41. use function str_contains;
  42. use function time;
  43. use function urldecode;
  44. use const JSON_THROW_ON_ERROR;
  45. class Controller extends AbstractController implements LoggerAwareInterface
  46. {
  47.     use LoggerAwareTrait;
  48.     private const COOKIE_NAME 'rotatorId';
  49.     private const ROTATOR_TTL 30;
  50.     private const OLD_COOKIE_NAME '__router';
  51.     private const COUNTRY_CODE_HEADER_NAME 'X-GeoIP-Country-Code';
  52.     private const BACK_URL 'backurl';
  53.     private const RETURN_URL 'returnUrl';
  54.     private const MONITORING_USER_AGENT_LIST = [
  55.         'Better Uptime Bot',
  56.         'Zabbix',
  57.         'sentry',
  58.         'GuzzleHttp',
  59.         'Google-PageRenderer',
  60.     ];
  61.     public function __construct(
  62.         private readonly MirrorService $mirrorService,
  63.         private readonly RotatorService $rotatorService,
  64.         private readonly PlayerTokenService $playerTokenService,
  65.         private readonly WlSettingsService $wlSettingsService,
  66.         private readonly WlRotatorDomainService $wlRotatorDomainService,
  67.         private readonly InfluxDbClient $influxDbClient,
  68.         private readonly MessageBusInterface $bus,
  69.         private readonly ControllerV2 $controllerV2,
  70.         private readonly IdGenerator $idGenerator,
  71.         private readonly ?string $whitelabel,
  72.     ) {
  73.     }
  74.     /**
  75.      * @Route("{path}", requirements={"path"=".*"}, methods={"GET"}, priority=-2)
  76.      *
  77.      * @throws DbalException
  78.      * @throws ExceptionInterface
  79.      * @throws InfluxDatabaseException
  80.      * @throws InfluxException
  81.      * @throws JsonException
  82.      */
  83.     public function index(Request $request, ?string $path null): Response
  84.     {
  85.         if ($this->whitelabel === null) {
  86.             return new Response('Unknown whitelabel. Please, set <b>HTTP_X_WHITELABEL</b> fastcgi_param');
  87.         }
  88.         $wlSlug $this->wlSettingsService->getRealWlSlug($this->whitelabel);
  89.         if ($wlSlug === 'real-betonred') { // TODO need to hardcode to use v2 for brand
  90.             return $this->controllerV2->indexV2($request$path);
  91.         }
  92.         $country $request->server->get(self::COUNTRY_CODE_HEADER_NAME);
  93.         $this->sendMetric('hit'$country);
  94.         $rotatorId $request->cookies->get(self::COOKIE_NAME);
  95.         $rotatorIdEncoded $request->query->get('_ri');
  96.         $rdParam $request->query->get('_rd');
  97.         if (is_string($rotatorIdEncoded) && !empty($rotatorIdEncoded)) {
  98.             $rotatorId base64_decode(urldecode($rotatorIdEncoded));
  99.         }
  100.         $rotatorRouter $request->getHost();
  101.         $rotatorRouterEncoded $request->query->get('_rr');
  102.         if ($rotatorRouterEncoded) {
  103.             $rotatorRouter base64_decode(urldecode($rotatorRouterEncoded));
  104.         }
  105.         if (is_string($rdParam)) {
  106.             try {
  107.                 $rdDecoded json_decode(base64_decode(urldecode($rdParam)), true512JSON_THROW_ON_ERROR);
  108.                 if (!empty($rdDecoded['_ri'])) {
  109.                     $rotatorId $rdDecoded['_ri'];
  110.                 }
  111.                 if (!empty($rdDecoded['_rr'])) {
  112.                     $rotatorRouter $rdDecoded['_rr'];
  113.                 }
  114.             } catch (Throwable $e) {
  115.                 $this->logger->error(
  116.                     'bad rdParam',
  117.                     [
  118.                         'rdParam' => $rdParam,
  119.                         'request' => $request,
  120.                         'exception' => (string) $e,
  121.                     ]
  122.                 );
  123.             }
  124.         }
  125.         $oldHost $request->cookies->get(self::OLD_COOKIE_NAME);
  126.         $oldRotatorCallback null;
  127.         $oldRotatorId null;
  128.         $rotator null;
  129.         $requestRotatorId $request->query->get(self::COOKIE_NAME);
  130.         $from $request->query->get('from');
  131.         $ts = (int) $request->query->get('ts');
  132.         $uuid $request->query->get('uuid');
  133.         $sign $request->query->get('sign');
  134.         $needTsUuidSignCheck false;
  135.         $query $request->query->all();
  136.         if ($requestRotatorId !== null) {
  137.             $rotatorId $requestRotatorId;
  138.             if ($from === null) {
  139.                 $needTsUuidSignCheck true;
  140.             }
  141.         }
  142.         $userAgent substr((string) $request->server->get('HTTP_USER_AGENT'), 0255);
  143.         foreach (self::MONITORING_USER_AGENT_LIST as $monitoringUserAgent) {
  144.             if (str_contains($userAgent$monitoringUserAgent)) {
  145.                 $rotatorId 'monitoring';
  146.                 $country 'DE';
  147.             }
  148.         }
  149.         if ($rotatorId) {
  150.             $rotator $this->rotatorService->findBySticky($rotatorId);
  151.             if ($rotator === null) {
  152.                 $rotator $this->rotatorService->findByCountry($rotatorId$country);
  153.             }
  154.             if ($rotator === null) {
  155.                 $rotator $this->rotatorService->findByOldRotatorCountry($rotatorId$country);
  156.             }
  157.             if ($rotator === null) {
  158.                 $oldRotator $this->rotatorService->find($rotatorId);
  159.                 if ($oldRotator !== null) {
  160.                     $oldRotatorCallback $oldRotator['callback'] ?? null;
  161.                     $oldRotatorId $rotatorId;
  162.                 }
  163.             }
  164.             $rotatorId $rotator['rotatorId'] ?? null;
  165.         }
  166.         //        if ($rotatorId === null) {
  167.         if (!empty($query[self::RETURN_URL])) { // logic for sticky host from returnUrl
  168.             $host false;
  169.             if (is_string($query[self::RETURN_URL])) {
  170.                 $host parse_url($query[self::RETURN_URL], PHP_URL_HOST);
  171.             }
  172.             if ($host !== false && $host !== null) {
  173.                 $mirror $this->mirrorService->findStickyHost($this->whitelabel$host);
  174.                 $this->logger->info(
  175.                     'mirror from redirect url',
  176.                     [
  177.                         'mirror' => is_array($mirror) ? implode(', '$mirror) : ($mirror ?? 'not found'),
  178.                         'rotatorId' => $rotatorId,
  179.                         'redirectUrl' => $query[self::RETURN_URL],
  180.                     ]
  181.                 );
  182.             }
  183.             if ($host === null) {
  184.                 $mirror $this->mirrorService->findStickyHost($this->whitelabel$query[self::RETURN_URL]);
  185.             }
  186.             if (empty($mirror)) { // redirect to returnUrl for non sticky
  187.                 if ($host !== false && $host !== null) {
  188.                     $mirror $this->mirrorService->findByWlCountryHost($this->whitelabel$country$host);
  189.                 }
  190.                 if ($host === null) {
  191.                     $mirror $this->mirrorService->findByWlCountryHost($this->whitelabel$country$query[self::RETURN_URL]);
  192.                 }
  193.                 if ($mirror === null && $host !== false && $host !== null) {
  194.                     $mirror $this->mirrorService->findByHost($host);
  195.                 }
  196.             }
  197.             // unset($query[self::RETURN_URL]);
  198.         }
  199.         if (empty($mirror) && $oldHost) {
  200.             $mirror $this->mirrorService->findByHost($oldHost);
  201.         }
  202.         if (empty($mirror)) {
  203.             $mirror $this->mirrorService->getRandom($this->whitelabel$country);
  204.         }
  205.         $rotatorId $rotatorId
  206.             ?? $this->rotatorService->create($mirror['id'], $country$userAgent$oldRotatorId);
  207.         //        }
  208.         if (empty($rotator)) {
  209.             $rotator $this->rotatorService->findByCountry($rotatorId$country);
  210.             if ($rotator !== null && $oldRotatorCallback !== null) {
  211.                 $this->rotatorService->updateRotatorCallback($rotator$oldRotatorCallback);
  212.                 $rotator['callback'] = $oldRotatorCallback;
  213.                 $this->sendMetric('old_rotator_callback'$country);
  214.             }
  215.         }
  216.         $mirrorStatus $rotator['mirrorStatus'] ?? MirrorStatusEnum::BLOCKED;
  217.         if (strtolower($mirrorStatus) !== MirrorStatusEnum::ACTIVE) {
  218.             $mirror $this->mirrorService->getRandom($this->whitelabel$country);
  219.             $this->rotatorService->update($rotator['id'], [
  220.                 'rotatorId' => $rotatorId,
  221.                 'country' => $country,
  222.                 'mirrorId' => $mirror['id'],
  223.             ]);
  224.             $rotator['mirrorHost'] = $mirror['host'];
  225.         }
  226.         // google auth state after redirect
  227.         $state $request->query->get('state');
  228.         if (!empty($state)) {
  229.             try {
  230.                 $state json_decode(urldecode($state), true512JSON_THROW_ON_ERROR);
  231.                 if (!empty($state['backurl'])) {
  232.                     $query[self::BACK_URL] = urldecode($state['backurl']);
  233.                 }
  234.             } catch (Throwable) {
  235.             }
  236.         }
  237.         $wlSettingsDto $this->wlSettingsService->getWlSettings($this->whitelabel);
  238.         if ($wlSettingsDto === null) {
  239.             return new JsonResponse(
  240.                 [
  241.                     'message' => 'WL settings not found',
  242.                 ],
  243.                 Response::HTTP_NOT_FOUND,
  244.             );
  245.         }
  246.         try {
  247.             $accessToken $this->getAccessToken(
  248.                 $rotator,
  249.                 $query,
  250.                 $wlSettingsDto,
  251.                 $needTsUuidSignCheck,
  252.                 $ts,
  253.                 $uuid,
  254.                 $sign,
  255.             );
  256.         } catch (Throwable) {
  257.             $accessToken null;
  258.         }
  259.         $isHostRouterEqual $request->getHost() === $wlSettingsDto->getDefaultRotatorDomain();
  260.         if ($accessToken !== null) {
  261.             $query['accessToken'] = $accessToken;
  262.             if ($isHostRouterEqual === true) {
  263.                 $this->sendMetric('access_token'$country$rotator['mirrorHost']);
  264.             }
  265.         }
  266.         if (!empty($query['rotatorToken'])) {
  267.             unset($query['rotatorToken']);
  268.         }
  269.         $redirect '/' $path;
  270.         $redirectHost $rotator['mirrorHost'];
  271.         if ($isHostRouterEqual === false) {
  272.             $redirectHost $wlSettingsDto->getDefaultRotatorDomain();
  273.             $query['rotatorId'] = $rotatorId;
  274.             $this->sendMetric('default_router_redirect'$country);
  275.         }
  276.         if (!empty($query[self::BACK_URL])) {
  277.             $redirect $query[self::BACK_URL];
  278.             unset($query[self::BACK_URL]);
  279.         } else if (!empty($query[self::RETURN_URL])) {
  280.             $pathFromReturnUrl parse_url($query[self::RETURN_URL], PHP_URL_PATH);
  281.             $queryString parse_url($query[self::RETURN_URL], PHP_URL_QUERY);
  282.             $redirect $pathFromReturnUrl . ($queryString '?' $queryString '');
  283.             if (!str_starts_with($redirect'/')) {
  284.                 $redirect '/' $redirect;
  285.             }
  286.             unset($query[self::RETURN_URL]);
  287.         }
  288.         if ($redirect[0] === '/') {
  289.             $redirect 'https://' trim($redirectHost) . $redirect;
  290.         }
  291.         try {
  292.             $query['_rd'] = urlencode(base64_encode(json_encode([
  293.                 'rotatorId' => $rotatorId,
  294.                 'rotatorRouter' => $rotatorRouter,
  295.             ], JSON_THROW_ON_ERROR)));
  296.         } catch (Throwable $e) {
  297.             $this->logger->critical(
  298.                 "Problem with _rd generation",
  299.                 [
  300.                     'rotatorId' => $rotatorId,
  301.                     'rotatorRouter' => $rotatorRouter,
  302.                     'exception' => $e,
  303.                     'request' => $request,
  304.                 ]
  305.             );
  306.             throw $e;
  307.         }
  308.         $redirect .= sprintf(
  309.             '%s%s',
  310.             str_contains($redirect'?') ? '&' '?',
  311.             http_build_query($query)
  312.         );
  313.         $response = new RedirectResponse($redirect);
  314.         $response->headers
  315.             ->setCookie(
  316.                 new Cookie(
  317.                     self::COOKIE_NAME,
  318.                     $rotatorId,
  319.                     time() + (365 24 60 60)
  320.                 )
  321.             )
  322.         ;
  323.         $this->sendMetric('redirect'$country$rotator['mirrorHost']);
  324.         $redirectData = [
  325.             'ip' => $request->getClientIp(),
  326.             'rotatorId' => $rotatorId,
  327.             'mirrorHost' => $redirectHost,
  328.             'rotatorRouter' => $request->getHost(),
  329.         ];
  330.         $this->logger->info('Redirect'$redirectData);
  331.         $this->bus->dispatch(new ApiRequestLog(
  332.             rotatorId$redirectData['rotatorId'],
  333.             ip$redirectData['ip'],
  334.             mirrorHost$redirectData['mirrorHost'],
  335.             rotator$redirectData['rotatorRouter'],
  336.             country$country,
  337.             request: (string) $request,
  338.             response: (string) $response,
  339.             responseTimetime() - $request->server->get('REQUEST_TIME'),
  340.             requestDatedate('Y-m-d H:i:s'$request->server->get('REQUEST_TIME')),
  341.             responseStatusCode$response->getStatusCode(),
  342.             uniqId$this->idGenerator->generateUniqueId(),
  343.         ));
  344.         return $response;
  345.     }
  346.     /**
  347.      * @Route("/callback", methods={"POST"})
  348.      *
  349.      * @param Request $request
  350.      * @return Response
  351.      * @throws DbalException
  352.      * @throws InfluxDatabaseException
  353.      * @throws InfluxException
  354.      * @throws JsonException
  355.      */
  356.     public function callbackBulk(Request $request): Response
  357.     {
  358.         try {
  359.             $body json_decode($request->getContent(), true512JSON_THROW_ON_ERROR);
  360.         } catch (JsonException) {
  361.             $body = [];
  362.         }
  363.         foreach ($body as $item) {
  364.             try {
  365.                 $this->handleCallback($item['rotatorId'], $item['callbackData']);
  366.             } catch (RotatorNotFoundException) {
  367.                 $this->logger->error("Rotator not found :" $item['rotatorId']);
  368.                 continue;
  369.             }
  370.         }
  371.         return new Response();
  372.     }
  373.     /**
  374.      * @Route("/callback/{rotatorId}", methods={"POST"})
  375.      *
  376.      * @throws DbalException
  377.      * @throws InfluxDatabaseException
  378.      * @throws InfluxException
  379.      * @throws JsonException
  380.      */
  381.     public function callback(Request $requeststring $rotatorId): Response
  382.     {
  383.         try {
  384.             $body json_decode($request->getContent(), true512JSON_THROW_ON_ERROR);
  385.         } catch (JsonException) {
  386.             $body = [];
  387.         }
  388.         try {
  389.             $this->handleCallback($rotatorId$body);
  390.         } catch (RotatorNotFoundException) {
  391.             $this->logger->error("Rotator not found :" $rotatorId);
  392.         }
  393.         return new Response();
  394.     }
  395.     /**
  396.      * @Route("/api/generateLinkToken", methods={"POST"})
  397.      */
  398.     public function generateLinkToken(Request $request): Response
  399.     {
  400.         try {
  401.             $body json_decode($request->getContent(), true512JSON_THROW_ON_ERROR);
  402.             $playerId $body['playerId'] ?? null;
  403.             $source $body['source'] ?? null;
  404.             $tokenCreatedAt = new DateTimeImmutable();
  405.             if ($playerId === null) {
  406.                 return new JsonResponse(
  407.                     [
  408.                         'message' => 'Player id is required',
  409.                     ],
  410.                     Response::HTTP_UNPROCESSABLE_ENTITY,
  411.                 );
  412.             }
  413.             if (!is_string($playerId)) {
  414.                 return new JsonResponse(
  415.                     [
  416.                         'message' => 'Player id must be string',
  417.                     ],
  418.                     Response::HTTP_UNPROCESSABLE_ENTITY,
  419.                 );
  420.             }
  421.             $wlSettingsDto $this->wlSettingsService->getWlSettings($this->whitelabel);
  422.             if ($wlSettingsDto === null) {
  423.                 return new JsonResponse(
  424.                     [
  425.                         'message' => 'WL settings not found',
  426.                     ],
  427.                     Response::HTTP_NOT_FOUND,
  428.                 );
  429.             }
  430.             $token sha1(
  431.                 sprintf(
  432.                     '%s%s%s',
  433.                     $playerId,
  434.                     $tokenCreatedAt->getTimestamp(),
  435.                     $wlSettingsDto->getSalt()
  436.                 )
  437.             );
  438.             $expiredAt = (new DateTimeImmutable())
  439.                 ->modify('+' $wlSettingsDto->getTokenLifetime() . ' seconds')
  440.             ;
  441.             $this->playerTokenService->createNewPlayerToken($playerId$this->whitelabel$token$expiredAt);
  442.             $wlRotatorDomainDto null;
  443.             if ($source !== null) {
  444.                 $wlRotatorDomainDto $this->wlRotatorDomainService->getWlRotatorDomainDtoBySource(
  445.                     $this->whitelabel,
  446.                     $source,
  447.                 );
  448.             }
  449.             if ($wlRotatorDomainDto === null) {
  450.                 $wlRotatorDomainDto $this->wlRotatorDomainService->getRandomWlRotatorDomainDto($this->whitelabel);
  451.                 if ($wlRotatorDomainDto === null) {
  452.                     return new JsonResponse(
  453.                         [
  454.                             'message' => 'WL rotator domain not found',
  455.                         ],
  456.                         Response::HTTP_NOT_FOUND,
  457.                     );
  458.                 }
  459.             }
  460.             $data = [
  461.                 'link' => sprintf('%s/?rotatorToken=%s'$wlRotatorDomainDto->getDomain(), $token),
  462.                 'endTime' => $expiredAt->getTimestamp(),
  463.                 'token' => $token,
  464.             ];
  465.             return new JsonResponse($dataResponse::HTTP_OK);
  466.         } catch (Throwable $e) {
  467.             return new JsonResponse($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
  468.         }
  469.     }
  470.     /**
  471.      * @throws InfluxException
  472.      * @throws InfluxDatabaseException
  473.      */
  474.     private function sendMetric(string $keystring $country, ?string $mirror null): void
  475.     {
  476.         $fields = [
  477.             'country' => $country,
  478.             'rotator' => $this->whitelabel,
  479.         ];
  480.         if ($mirror) {
  481.             $fields['mirror'] = $mirror;
  482.         }
  483.         $this->influxDbClient->sendBulk($key$fields$fields);
  484.     }
  485.     /**
  486.      * @throws InfluxException
  487.      * @throws JsonException
  488.      * @throws DbalException
  489.      * @throws InfluxDatabaseException
  490.      * @throws RotatorNotFoundException
  491.      */
  492.     private function handleCallback(string $rotatorId, array $callbackData): void
  493.     {
  494.         $rotator $this->rotatorService->find($rotatorId);
  495.         if ($rotator === null) {
  496.             throw new RotatorNotFoundException();
  497.         }
  498.         $isPlayerIdMismatch = !empty($callbackData['playerId'])
  499.             && !empty($rotator['playerId'])
  500.             && $callbackData['playerId'] !== $rotator['playerId']
  501.         ;
  502.         if ($isPlayerIdMismatch) {
  503.             return;
  504.         }
  505.         $isPlayerUuidMismatch = !empty($callbackData['playerUuid'])
  506.             && !empty($rotator['playerUuid'])
  507.             && $callbackData['playerUuid'] !== $rotator['playerUuid']
  508.         ;
  509.         if ($isPlayerUuidMismatch) {
  510.             return;
  511.         }
  512.         $country $rotator['country'] ?? '';
  513.         $this->sendMetric('callback'$country$rotator['mirrorHost']);
  514.         $data = [
  515.             'rotatorId' => $rotator['rotatorId'],
  516.             'country' => $country,
  517.             'callback' => json_encode($callbackDataJSON_THROW_ON_ERROR),
  518.             'callbackAt' => (new DateTimeImmutable())->format('Y-m-d H:i:s'),
  519.         ];
  520.         if (empty($rotator['playerId']) && !empty($callbackData['playerId'])) {
  521.             $data['playerId'] = $callbackData['playerId'];
  522.         }
  523.         if (empty($rotator['playerUuid']) && !empty($callbackData['playerUuid'])) {
  524.             $data['playerUuid'] = $callbackData['playerUuid'];
  525.         }
  526.         if (!empty($callbackData['host'])) {
  527.             $mirror $this->mirrorService->findByHost($callbackData['host']);
  528.             if ($mirror !== null && $rotator['mirrorId'] !== $mirror['id']) {
  529.                 $data['mirrorId'] = $mirror['id'];
  530.             }
  531.         }
  532. /*
  533.         try {
  534.             $this->bus->dispatch(new CallbackLog(
  535.                 rotatorId: $rotator['rotatorId'],
  536.                 callbackRaw: json_encode($callbackData, JSON_THROW_ON_ERROR),
  537.                 updatedData: json_encode($data, JSON_THROW_ON_ERROR),
  538.                 callbackAt: date('Y-m-d H:i:s'),
  539.                 uniqId: $this->idGenerator->generateUniqueId(),
  540.             ));
  541.         } catch (Throwable $e) {
  542.             $this->logger->critical($e->getMessage());
  543.         }
  544. */
  545.         $this->rotatorService->update($rotator['id'], $data);
  546.     }
  547.     private function getAccessToken(
  548.         array $rotator,
  549.         $query,
  550.         WlSettingsDto $wlSettingsDto,
  551.         bool $needTsUuidSignCheck,
  552.         ?int $ts,
  553.         ?string $uuid,
  554.         ?string $sign,
  555.     ): ?string {
  556.         if (
  557.             $needTsUuidSignCheck &&
  558.             $this->checkTsUuidSign(
  559.                 $rotator,
  560.                 $wlSettingsDto->getSecret(),
  561.                 $ts,
  562.                 $uuid,
  563.                 $sign,
  564.             ) === false
  565.         ) {
  566.             return null;
  567.         }
  568.         try {
  569.             return $this->rotatorService->getAccessToken($rotator$query$wlSettingsDto);
  570.         } catch (Throwable) {
  571.         }
  572.         return null;
  573.     }
  574.     private function checkTsUuidSign(
  575.         array $rotator,
  576.         ?string $secret,
  577.         ?int $ts,
  578.         ?string $uuid,
  579.         ?string $sign,
  580.     ): bool {
  581.         if (empty($ts) || empty($uuid) || empty($sign)) {
  582.             return false;
  583.         }
  584.         if (time() - floor($ts 1000) > self::ROTATOR_TTL) {
  585.             return false;
  586.         }
  587.         if (empty($secret)) {
  588.             return true;
  589.         }
  590.         if ($sign !== sha1($rotator['rotatorId'] . $ts $uuid $secret)) {
  591.            return false;
  592.         }
  593.         if (!empty($rotator['playerUuid']) && $uuid !== $rotator['playerUuid']) {
  594.             return false;
  595.         }
  596.         return true;
  597.     }
  598. }