Viewing File: /usr/local/cpanel/whostmgr/docroot/cgi/ncssl/source/src/Command/RpcHandlerCommand.php

<?php

namespace App\Command;

use App\RpcListeners\RpcListenerException;
use App\RpcListeners\RpcListenerInterface;
use App\Service\Certificate\CertificateTransfer;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption as InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validation;

#[AsCommand(name: 'app:rpc-handler', description: 'Handle external request')]
class RpcHandlerCommand extends Command
{
    public const VERSION = '1.0.0';

    public const BACKGROUND_LISTENERS = [
        'newDomainHandler'
    ];

    public const BACKGROUND_EVENTS = [
        'newAccount',
    ];

    public const BACKGROUND_PROCESS_START_NOTIFICATION_PATTERN = 'Background Process Started. Server: "%s", User: "%s", Domain: "%s"';
    public const ERROR_WRONG_PARAMETERS = 'No required parameter(s) set';
    public const ERROR_WRONG_METHOD = 'Wrong method requested';

    public function __construct(
        private readonly LoggerInterface $rpcHandlerLogger,
        #[Autowire(param: 'rpcHandler.backgroundJobsPath')]
        private readonly string $backgroundJobsPath,
        #[Autowire(service: 'service_container')]
        private readonly ContainerInterface $container,
    ){
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addArgument(
                'data',
                null,
                InputArgument::VALUE_REQUIRED);
    }

    /**
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return int
     * @throws \JsonException
     * @throws RpcListenerException
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $data = $input->getArgument('data');
        $data = json_decode(base64_decode($data), true, 512, JSON_THROW_ON_ERROR);
        $this->rpcHandlerLogger->notice('RpcHandler argument', $data);

        if ($this->mustBeRunInBackground($data)) {
            $this->storeForBackgroundExecution($data);
            return Command::SUCCESS;
        }

        if (!$this->validateProtocolParameters($data)) {
            $this->rpcHandlerLogger->error(self::ERROR_WRONG_PARAMETERS, $data);
            $output->write($this->getErrorResponse(Response::HTTP_UNAUTHORIZED, self::ERROR_WRONG_PARAMETERS));
            return Command::SUCCESS;
        }

        $handler = $this->getListener($data['method']);

        if (!$handler) {
            $this->rpcHandlerLogger->error(self::ERROR_WRONG_METHOD, $data);
            $output->write($this->getErrorResponse(Response::HTTP_NOT_FOUND, self::ERROR_WRONG_METHOD, ['method' => $data['method']]));
            return Command::SUCCESS;
        }

        try {
            $transfer = new CertificateTransfer($data['params']);

            $this->rpcHandlerLogger->notice(sprintf(self::BACKGROUND_PROCESS_START_NOTIFICATION_PATTERN,
                $transfer->getCPanelServer(),
                $transfer->getCPanelUser(),
                $transfer->getDomainName()));

            $transfer = $handler->listen($transfer);
        } catch (RpcListenerException $e) {
            $this->rpcHandlerLogger->error(get_class($handler) . ' error: ' . $e->getMessage(), $e->getData());
            $output->write($this->getErrorResponse($e->getCode(), $e->getMessage(), $e->getData()));
            return Command::FAILURE;
        }

        $output->writeln($this->getResponse($transfer));

        return Command::SUCCESS;
    }

    /**
     * @param array $parameters
     * @param mixed $scheme
     *
     * @return void
     * @throws RpcListenerException
     */
    protected function checkValidation(array $parameters, mixed $scheme): void
    {
        $validator = Validation::createValidator();
        $errors = $validator->validate($parameters, $scheme);

        if (count($errors) > 0) {
            throw new RpcListenerException('Invalid parameters', 0, null, $parameters);
        }
    }

    /**
     * @param array $request
     *
     * @return bool
     */
    private function validateProtocolParameters(array $request): bool
    {
        $scheme = new Assert\Collection([
            'version' => new Assert\Required([
                new Assert\NotBlank(),
                new Assert\Type('string'),
            ]),
            'method' => new Assert\Required([
                new Assert\NotBlank(),
                new Assert\Type('string'),
            ]),
            'params' => new Assert\Required([
                new Assert\NotBlank(),
                new Assert\Type('array'),
            ]),
        ], allowExtraFields: true);

        try {
            $this->checkValidation($request, $scheme);
        } catch (RpcListenerException $exception) {
            $this->rpcHandlerLogger->error($exception->getMessage(), $request);

            return false;
        }

        return true;
    }

    /**
     * @param string $handlerName
     *
     * @return object|null
     * @throws RpcListenerException
     */
    private function getListener(string $handlerName): ?RpcListenerInterface
    {
        if ($this->container->has($handlerName)) {
            $listener = $this->container->get($handlerName);

            if ($listener instanceof RpcListenerInterface) {
                return $listener;
            }
        }

        throw new RpcListenerException('Listener not found: {$handlerName}');
    }

    /**
     * Get MS protocol success response
     *
     * @param CertificateTransfer $result
     *
     * @return string
     * @throws \JsonException
     */
    private function getResponse(CertificateTransfer $result): string
    {
        $response = [
            'version' => self::VERSION,
            'meta' => $this->getMeta(),
            'result' => [
                'id' => $result->getCertificateId(),
                'type' => $result->getCertType(),
                'domain' => $result->getDomainName()
            ],
        ];

        return json_encode($response, JSON_THROW_ON_ERROR);
    }

    /**
     * Get MS protocol error response
     *
     * @param int $code
     * @param string $message
     * @param array $data
     *
     * @return string
     * @throws \JsonException
     */
    private function getErrorResponse(int $code, string $message, array $data = []): string
    {
        $response = [
            'version' => self::VERSION,
            'meta' => $this->getMeta(),
            'error' => [
                'code' => (int)$code,
                'message' => $message,
                'data' => $data,
            ],
        ];

        return json_encode($response, JSON_THROW_ON_ERROR);
    }

    private function getMeta(): array
    {
        return [
            'timestamp' => time(),
            'server' => gethostname(),
        ];
    }

    /**
     * @param array $data
     *
     * @return bool
     */
    private function mustBeRunInBackground(array $data): bool
    {
        $isBackgroundHandler = in_array($data['method'], self::BACKGROUND_LISTENERS, true);
        $isBackgroundEvent = isset($data['params']['eventName']) && in_array($data['params']['eventName'], self::BACKGROUND_EVENTS, true);

        return $isBackgroundHandler && $isBackgroundEvent;
    }

    /**
     * @throws \JsonException
     */
    private function storeForBackgroundExecution(array $data): void
    {
        unset($data['params']['eventName']);
        $jsonData = json_encode($data, JSON_THROW_ON_ERROR | true);
        file_put_contents($this->backgroundJobsPath, $jsonData . PHP_EOL, FILE_APPEND | LOCK_EX);
    }
}
Back to Directory File Manager