Viewing File: /usr/local/cpanel/whostmgr/docroot/cgi/ncssl/source/src/Service/Certificate/AbstractActivate.php

<?php

namespace App\Service\Certificate;

use App\Entity\User;
use App\Exception\ActivateException;
use App\Entity\Certificate as CertificateEntity;
use App\Service\CpanelHelper;
use App\Service\NcGatewayApi\Exceptions\NcGatewayApiException;
use App\Service\NcGatewayApi\NcGatewayApi;
use App\Service\NcPlugin\PluginException;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;

abstract class AbstractActivate
{
    public const CSR_COUNTRY_NAME = 'US';
    public const CSR_LOCALITY = 'Phoenix';
    public const CSR_STATE = 'Arizona';

    public const MESSAGE_START_ACTIVATION = 'Start activation for certId: %s';
    public const MESSAGE_START_REISSUE = 'Start reissue for certId: %s';
    public const MESSAGE_GET_ROOT_FOLDER = 'Get Root folder for domain: %s';
    public const MESSAGE_GENERATE_PRIVATE_KEY = 'Generate private key';
    public const MESSAGE_GENERATE_CSR = 'Generate CSR';
    public const MESSAGE_RENAME_PRIVATE_KEY = 'Rename private key';
    public const MESSAGE_EXTERNAL_CALL = 'Make external call';
    public const MESSAGE_SAVE_CERTIFICATE = 'Save certificate: %s';
    public const MESSAGE_SAVE_HTTP_DCV_FILE = 'Save HttpDcvFile for certificateId: %s';
    public const START_ASYNC_ACTIVATION_MESSAGE = 'Start async activation for certId: %s';
    public const CHANGE_CERTIFICATE_STATUS_TO_NEW_MESSAGE = 'Certificate status has been changed to new, for certId: %s';
    public const MESSAGE_UNABLE_COMPLETE_DCV = 'SSL installation failed due to %s being missing from cPanel or a server access issue. Please <a href="https://support.namecheap.com/index.php?/Tickets/Submit" target="_blank" class="btn-link">contact support</a>';
    public const MESSAGE_INVALID_DCV_FILE_VALIDATION = 'Invalid HttpDcvFile validation: %s';
    public const MESSAGE_CANT_DEFINE_CPANEL_USER = 'Can\'t define cPanel user';
    public const MESSAGE_NC_USER_MISSING = 'Nc user name missing';
    public const MESSAGE_USER_NOT_FOUND = 'User not found in database for activate certificate';

    private string $privateKey;
    private string $csr;
    private string $cpanelUser;

    abstract protected function makeExternalCall(CertificateTransfer $certificateTransfer, string $csr, bool $isAsyncExternalCall): void;

    public function __construct(protected CpanelHelper $cpanelHelper, protected NcGatewayApi $ncGatewayApi, protected EntityManagerInterface $entityManager, protected LoggerInterface $logger, protected ProductManager $productManager) { }

    /**
     * @throws ActivateException
     */
    protected function makeAction(CertificateTransfer $data, bool $isAsyncExternalCall = false): void
    {
        try {
            $this->generateCsr($data->getCertificateId(), $this->performCsrData($data->getCommonName()));

            $this->logger->notice(self::MESSAGE_EXTERNAL_CALL);
            $this->makeExternalCall($data, $this->getCsrRequest(), $isAsyncExternalCall);

            $this->logger->notice(self::MESSAGE_RENAME_PRIVATE_KEY);
            $this->cpanelHelper->renamePrivateKey($this->privateKey, $data->getCertificateId());

            $this->logger->notice(sprintf(self::MESSAGE_SAVE_CERTIFICATE, $data->getCertificateId()), (array) $data);

            if (!$data->getNCUser()) {
                $this->logger->error(self::MESSAGE_NC_USER_MISSING, [
                    'ncLogin' => $data->getNCUser(),
                    'certificateId' => $data->getCertificateId(),
                ]);

                throw new PluginException(self::MESSAGE_NC_USER_MISSING);
            }

            $user = $this->getUserFromRepository($data->getCPanelUser(), $data->getNCUser());

            if (!$user) {
                // if user have never logged to plugin he doesn't have a ncLogin in db
                $user = $this->getUserFromRepository($data->getCPanelUser());

                if (!$user) {
                    $this->logger->error(self::MESSAGE_USER_NOT_FOUND, [
                        'ncLogin' => $data->getNCUser(),
                        'certificateId' => $data->getCertificateId(),
                    ]);

                    throw new PluginException(self::MESSAGE_USER_NOT_FOUND);
                }

                $user->setNcLogin($data->getNCUser());
                $this->logger->notice(sprintf('Set ncLogin for user: %s', $user->getName()), [
                    'ncLogin' => $data->getNCUser(),
                ]);
            }

            $certificate = new CertificateEntity();
            $certificate->setUser($user);
            $certificate->setNcId($data->getCertificateId());
            $certificate->setNcStatus($data->getNCStatus());
            $certificate->setNcUser($data->getNCUser());
            $certificate->setHost($this->getHostName($data->getDomainName()));
            $certificate->setCommonName($data->getCommonName());
            $certificate->setType($data->getCertType());
            $certificate->setYears($data->getYears());
            $certificate->setVendor($data->getVendor());
            $certificate->setStatus(CertificateEntity::STATUS_INPROGRESS);
            $certificate->setPrivatekeyId($this->getPrivateKey());
            $certificate->setValidationData($this->prepareValidationData($data));
            $certificate->setAutoRedirect((bool)$user->getAutoRedirect());

            $this->entityManager->persist($certificate);
            $this->entityManager->flush();

            if ($this->checkValidationFile($data)) {
                $this->saveHttpDcvFile($data);
            }
        } catch (\Throwable $exception) {
            if ($exception instanceof NcGatewayApiException) {
                $message = sprintf('Plugin Exception error: %s, code: %s', $exception->getMessage(), $exception->getCode());
            } else {
                $message = sprintf('Plugin Exception error: %s', $exception->getMessage());
            }

            $this->logger->error($message, [$data->getDomainName()]);

            throw new ActivateException($exception->getMessage(), $exception->getCode(), $exception);
        }
    }

    /**
     * @param CertificateTransfer $certificateTransfer
     *
     * @throws PluginException
     */
    public function saveHttpDcvFile(CertificateTransfer $certificateTransfer): void
    {
        $this->logger->notice(sprintf(self::MESSAGE_SAVE_HTTP_DCV_FILE, $certificateTransfer->getCertificateId()));
        $this->cpanelUser = $certificateTransfer->getCPanelUser();

        try {
            $domainDocRoot = $this->getDomainDocRoot($certificateTransfer->getDomainName());
        } catch (\Exception $e) {
            $this->logger->error(sprintf('GetDomainDocRoot exception: %s', $e->getMessage()));
            throw new PluginException(sprintf(self::MESSAGE_UNABLE_COMPLETE_DCV, $certificateTransfer->getDomainName()));
        }

        $filePath = $this->getDcvFilePath($domainDocRoot) . '/' . $certificateTransfer->getFileName();
        $this->logger->notice(sprintf('saveHttpDcvFile path: %s', $filePath));
        $write_res = file_put_contents($filePath, $certificateTransfer->getFileContent());
        if ($write_res === false) {
            throw new PluginException(sprintf(self::MESSAGE_UNABLE_COMPLETE_DCV, $certificateTransfer->getDomainName()));
        }
        $scanDirResults = print_r(scandir($this->getDcvFilePath($domainDocRoot)), true);
        $this->logger->notice(sprintf('saveHttpDcvFile [scan dir]: %s', $scanDirResults));

        if ($this->cpanelUser) {
            chown($filePath, $this->cpanelUser);
            chgrp($filePath, $this->cpanelUser);
        }
    }

    /**
     * @param string $rootPath
     * @throws PluginException
     */
    protected function validateDcvDataPath(string $rootPath): void
    {
        $this->getDcvFilePath($rootPath);

        if (!$this->checkDirWritable($rootPath)) {
            throw new PluginException('Can\'t create validation file in host docroot folder. [' . $rootPath . ']');
        }
    }

    /**
     * @param string $domainName
     *
     * @return mixed|null
     *
     * @throws PluginException
     */
    public function getDomainDocRoot(string $domainName): mixed
    {
        $this->logger->notice(sprintf(self::MESSAGE_GET_ROOT_FOLDER, $domainName));
        $domain = $this->getHostName($domainName);
        $domainDocRoot = $this->cpanelHelper->getDomainDocRoot($domain);
        $this->validateDcvDataPath($domainDocRoot);

        return $domainDocRoot;
    }

    protected function getDcvFilePath(string $docRoot): string
    {
        $dcvDataPath = '/.well-known/pki-validation';
        $destinationPath = $docRoot . $dcvDataPath;

        if (!is_dir($destinationPath)) {
            if (!mkdir($destinationPath, 0755, true) && !is_dir($destinationPath)) {
                throw new \RuntimeException(sprintf('Directory "%s" was not created', $destinationPath));
            }
        }

        $this->changeOwner($docRoot, $dcvDataPath);

        return $destinationPath;
    }

    /**
     * @param string $root
     * @param string $path
     */
    protected function changeOwner(string $root, string $path): void
    {
        if (!$this->cpanelUser) {
            return;
        }

        $path = trim($path, '/');
        $chunks = explode('/', $path);

        $curPath = $root . '/' . array_shift($chunks);

        chown($curPath, $this->cpanelUser);
        chgrp($curPath, $this->cpanelUser);

        if (count($chunks)) {
            $this->changeOwner($curPath, implode('/', $chunks));
        }
    }

    /**
     * @param string $dir
     * @return bool
     */

    protected function checkDirWritable(string $dir): bool
    {
        $tst_filename = md5(time());
        $write_res = file_put_contents($dir . '/' . $tst_filename, $tst_filename);

        if ($write_res === false) {
            return false;
        }

        unlink($dir . '/' . $tst_filename);

        return true;
    }

    /**
     * @param string $host
     * @return string
     */
    public function getHostName(string $host = ''): string
    {
        if (str_starts_with($host, 'www.')) {
            $host = substr($host, 4);
        }

        return $host;
    }

    /**
     * @param string $certificateId
     * @param array $csrDetails
     * @throws PluginException
     */
    public function generateCsr(string $certificateId, array $csrDetails = []): void
    {
        $this->logger->notice(self::MESSAGE_GENERATE_PRIVATE_KEY);
        $this->privateKey = $this->cpanelHelper->generatePrivateKey($certificateId);

        $this->logger->notice(self::MESSAGE_GENERATE_CSR);
        $this->csr = $this->cpanelHelper->generateCsr($this->privateKey, $csrDetails);
    }

    public function getCsrRequest(): ?string
    {
        return $this->csr;
    }

    public function getPrivateKey(): string
    {
        return $this->privateKey;
    }

    /**
     * @param string $commonName
     * @return array
     */
    public function performCsrData(string $commonName = ''): array
    {
        return [
            'domains' => $commonName,
            'countryName' => self::CSR_COUNTRY_NAME,
            'stateOrProvinceName' => self::CSR_STATE,
            'localityName' => self::CSR_LOCALITY,
            'organizationName' => $commonName,
        ];
    }

    public static function getFormPrefilling(array $userInfo): array
    {
        return [
            'email' => $userInfo['default_address']['email'],
        ];
    }

    /**
     * @throws \JsonException
     */
    protected function prepareValidationData(CertificateTransfer $certificateTransfer): string
    {
        if (!$this->checkValidationFile($certificateTransfer)) {
            return '';
        }

        return json_encode([
            'filename' => $certificateTransfer->getFileName(),
            'content' => $certificateTransfer->getFileContent(),
        ], JSON_THROW_ON_ERROR);
    }

    protected function checkValidationFile(CertificateTransfer $certificateTransfer): bool
    {
        return $certificateTransfer->getFileContent() && $certificateTransfer->getFileName();
    }

    protected function getUserFromRepository(string $cPanelUser, string $ncUser = null): ?User
    {
        return $this->entityManager->getRepository(User::class)->findOneBy([
            'name' => $cPanelUser,
            'ncLogin' => $ncUser,
        ]);
    }
}
Back to Directory File Manager