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