diff --git a/.travis.yml b/.travis.yml index 0629e94a..2d954556 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ before_script: script: - cd "$TRAVIS_BUILD_DIR/../easyengine" - - ./vendor/bin/behat + - sudo ./vendor/bin/behat after_script: - cat /opt/easyengine/ee.log diff --git a/AcmePhp/Cli/Exception/AcmeCliActionException.php b/AcmePhp/Cli/Exception/AcmeCliActionException.php deleted file mode 100644 index a18f3e6f..00000000 --- a/AcmePhp/Cli/Exception/AcmeCliActionException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Exception; - -/** - * @author Titouan Galopin - */ -class AcmeCliActionException extends AcmeCliException -{ - public function __construct($actionName, \Exception $previous = null) - { - parent::__construct(sprintf('An exception was thrown during action "%s"', $actionName), $previous); - } -} diff --git a/AcmePhp/Cli/Exception/AcmeCliException.php b/AcmePhp/Cli/Exception/AcmeCliException.php deleted file mode 100644 index b5f4c0c7..00000000 --- a/AcmePhp/Cli/Exception/AcmeCliException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Exception; - -/** - * @author Titouan Galopin - */ -class AcmeCliException extends \RuntimeException -{ - public function __construct($message, \Exception $previous = null) - { - parent::__construct($message, 0, $previous); - } -} diff --git a/AcmePhp/Cli/Exception/AcmeDnsResolutionException.php b/AcmePhp/Cli/Exception/AcmeDnsResolutionException.php deleted file mode 100644 index a1137999..00000000 --- a/AcmePhp/Cli/Exception/AcmeDnsResolutionException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Exception; - -/** - * @author Jérémy Derussé - */ -class AcmeDnsResolutionException extends AcmeCliException -{ - public function __construct($message, \Exception $previous = null) - { - parent::__construct(null === $message ? 'An exception was thrown during resolution of DNS' : $message, $previous); - } -} diff --git a/AcmePhp/Cli/Exception/CommandFlowException.php b/AcmePhp/Cli/Exception/CommandFlowException.php deleted file mode 100644 index f03b5b29..00000000 --- a/AcmePhp/Cli/Exception/CommandFlowException.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Exception; - -/** - * @author Jérémy Derussé - */ -class CommandFlowException extends AcmeCliException -{ - /** - * @var string - */ - private $missing; - /** - * @var string - */ - private $command; - /** - * @var array - */ - private $arguments; - - /** - * @param string $missing Missing requirement to fix the flow - * @param string $command Name of the command to run in order to fix the flow - * @param array $arguments Optional list of missing arguments - * @param \Exception|null $previous - */ - public function __construct($missing, $command, array $arguments = [], \Exception $previous = null) - { - $this->missing = $missing; - $this->command = $command; - $this->arguments = $arguments; - - $message = trim(sprintf( - 'You have to %s first. Run the command%sphp %s %s %s', - $missing, - PHP_EOL.PHP_EOL, - $_SERVER['PHP_SELF'], - $command, - implode(' ', $arguments) - )); - - parent::__construct($message, $previous); - } -} diff --git a/AcmePhp/Cli/Repository/Repository.php b/AcmePhp/Cli/Repository/Repository.php deleted file mode 100644 index 29a54c18..00000000 --- a/AcmePhp/Cli/Repository/Repository.php +++ /dev/null @@ -1,439 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Repository; - -use AcmePhp\Cli\Exception\AcmeCliException; -use AcmePhp\Cli\Serializer\PemEncoder; -use AcmePhp\Core\Protocol\AuthorizationChallenge; -use AcmePhp\Core\Protocol\CertificateOrder; -use AcmePhp\Ssl\Certificate; -use AcmePhp\Ssl\CertificateResponse; -use AcmePhp\Ssl\DistinguishedName; -use AcmePhp\Ssl\KeyPair; -use AcmePhp\Ssl\PrivateKey; -use AcmePhp\Ssl\PublicKey; -use League\Flysystem\FilesystemInterface; -use Symfony\Component\Serializer\Encoder\JsonEncoder; -use Symfony\Component\Serializer\SerializerInterface; - -/** - * @author Titouan Galopin - */ -class Repository implements RepositoryV2Interface -{ - const PATH_ACCOUNT_KEY_PRIVATE = 'account/key.private.pem'; - const PATH_ACCOUNT_KEY_PUBLIC = 'account/key.public.pem'; - - const PATH_DOMAIN_KEY_PUBLIC = 'certs/{domain}/private/key.public.pem'; - const PATH_DOMAIN_KEY_PRIVATE = 'certs/{domain}/private/key.private.pem'; - const PATH_DOMAIN_CERT_CERT = 'certs/{domain}/public/cert.pem'; - const PATH_DOMAIN_CERT_CHAIN = 'certs/{domain}/public/chain.pem'; - const PATH_DOMAIN_CERT_FULLCHAIN = 'certs/{domain}/public/fullchain.pem'; - const PATH_DOMAIN_CERT_COMBINED = 'certs/{domain}/private/combined.pem'; - - const PATH_CACHE_AUTHORIZATION_CHALLENGE = 'var/{domain}/authorization_challenge.json'; - const PATH_CACHE_DISTINGUISHED_NAME = 'var/{domain}/distinguished_name.json'; - const PATH_CACHE_CERTIFICATE_ORDER = 'var/{domains}/certificate_order.json'; - - /** - * @var SerializerInterface - */ - private $serializer; - - /** - * @var FilesystemInterface - */ - private $master; - - /** - * @var FilesystemInterface - */ - private $backup; - - /** - * @var bool - */ - private $enableBackup; - - /** - * @param SerializerInterface $serializer - * @param FilesystemInterface $master - * @param FilesystemInterface $backup - * @param bool $enableBackup - */ - public function __construct(SerializerInterface $serializer, FilesystemInterface $master, FilesystemInterface $backup, $enableBackup) - { - $this->serializer = $serializer; - $this->master = $master; - $this->backup = $backup; - $this->enableBackup = $enableBackup; - } - - /** - * {@inheritdoc} - */ - public function storeCertificateResponse(CertificateResponse $certificateResponse) - { - $distinguishedName = $certificateResponse->getCertificateRequest()->getDistinguishedName(); - $domain = $distinguishedName->getCommonName(); - - $this->storeDomainKeyPair($domain, $certificateResponse->getCertificateRequest()->getKeyPair()); - $this->storeDomainDistinguishedName($domain, $distinguishedName); - $this->storeDomainCertificate($domain, $certificateResponse->getCertificate()); - } - - /** - * {@inheritdoc} - */ - public function storeAccountKeyPair(KeyPair $keyPair) - { - try { - $this->save( - self::PATH_ACCOUNT_KEY_PUBLIC, - $this->serializer->serialize($keyPair->getPublicKey(), PemEncoder::FORMAT) - ); - - $this->save( - self::PATH_ACCOUNT_KEY_PRIVATE, - $this->serializer->serialize($keyPair->getPrivateKey(), PemEncoder::FORMAT) - ); - } catch (\Exception $e) { - throw new AcmeCliException('Storing of account key pair failed', $e); - } - } - - private function getPathForDomain($path, $domain) - { - return strtr($path, ['{domain}' => $this->normalizeDomain($domain)]); - } - - private function getPathForDomainList($path, array $domains) - { - return strtr($path, ['{domains}' => $this->normalizeDomainList($domains)]); - } - - /** - * {@inheritdoc} - */ - public function hasAccountKeyPair() - { - return $this->master->has(self::PATH_ACCOUNT_KEY_PRIVATE); - } - - /** - * {@inheritdoc} - */ - public function loadAccountKeyPair() - { - try { - $publicKeyPem = $this->master->read(self::PATH_ACCOUNT_KEY_PUBLIC); - $privateKeyPem = $this->master->read(self::PATH_ACCOUNT_KEY_PRIVATE); - - return new KeyPair( - $this->serializer->deserialize($publicKeyPem, PublicKey::class, PemEncoder::FORMAT), - $this->serializer->deserialize($privateKeyPem, PrivateKey::class, PemEncoder::FORMAT) - ); - } catch (\Exception $e) { - throw new AcmeCliException('Loading of account key pair failed', $e); - } - } - - /** - * {@inheritdoc} - */ - public function storeDomainKeyPair($domain, KeyPair $keyPair) - { - try { - $this->save( - $this->getPathForDomain(self::PATH_DOMAIN_KEY_PUBLIC, $domain), - $this->serializer->serialize($keyPair->getPublicKey(), PemEncoder::FORMAT) - ); - - $this->save( - $this->getPathForDomain(self::PATH_DOMAIN_KEY_PRIVATE, $domain), - $this->serializer->serialize($keyPair->getPrivateKey(), PemEncoder::FORMAT) - ); - } catch (\Exception $e) { - throw new AcmeCliException(sprintf('Storing of domain %s key pair failed', $domain), $e); - } - } - - /** - * {@inheritdoc} - */ - public function hasDomainKeyPair($domain) - { - return $this->master->has($this->getPathForDomain(self::PATH_DOMAIN_KEY_PRIVATE, $domain)); - } - - /** - * {@inheritdoc} - */ - public function loadDomainKeyPair($domain) - { - try { - $publicKeyPem = $this->master->read($this->getPathForDomain(self::PATH_DOMAIN_KEY_PUBLIC, $domain)); - $privateKeyPem = $this->master->read($this->getPathForDomain(self::PATH_DOMAIN_KEY_PRIVATE, $domain)); - - return new KeyPair( - $this->serializer->deserialize($publicKeyPem, PublicKey::class, PemEncoder::FORMAT), - $this->serializer->deserialize($privateKeyPem, PrivateKey::class, PemEncoder::FORMAT) - ); - } catch (\Exception $e) { - throw new AcmeCliException(sprintf('Loading of domain %s key pair failed', $domain), $e); - } - } - - /** - * {@inheritdoc} - */ - public function storeDomainAuthorizationChallenge($domain, AuthorizationChallenge $authorizationChallenge) - { - try { - $this->save( - $this->getPathForDomain(self::PATH_CACHE_AUTHORIZATION_CHALLENGE, $domain), - $this->serializer->serialize($authorizationChallenge, JsonEncoder::FORMAT) - ); - } catch (\Exception $e) { - throw new AcmeCliException(sprintf('Storing of domain %s authorization challenge failed', $domain), $e); - } - } - - /** - * {@inheritdoc} - */ - public function hasDomainAuthorizationChallenge($domain) - { - return $this->master->has($this->getPathForDomain(self::PATH_CACHE_AUTHORIZATION_CHALLENGE, $domain)); - } - - /** - * {@inheritdoc} - */ - public function loadDomainAuthorizationChallenge($domain) - { - try { - $json = $this->master->read($this->getPathForDomain(self::PATH_CACHE_AUTHORIZATION_CHALLENGE, $domain)); - - return $this->serializer->deserialize($json, AuthorizationChallenge::class, JsonEncoder::FORMAT); - } catch (\Exception $e) { - throw new AcmeCliException(sprintf('Loading of domain %s authorization challenge failed', $domain), $e); - } - } - - /** - * {@inheritdoc} - */ - public function storeDomainDistinguishedName($domain, DistinguishedName $distinguishedName) - { - try { - $this->save( - $this->getPathForDomain(self::PATH_CACHE_DISTINGUISHED_NAME, $domain), - $this->serializer->serialize($distinguishedName, JsonEncoder::FORMAT) - ); - } catch (\Exception $e) { - throw new AcmeCliException(sprintf('Storing of domain %s distinguished name failed', $domain), $e); - } - } - - /** - * {@inheritdoc} - */ - public function hasDomainDistinguishedName($domain) - { - return $this->master->has($this->getPathForDomain(self::PATH_CACHE_DISTINGUISHED_NAME, $domain)); - } - - /** - * {@inheritdoc} - */ - public function loadDomainDistinguishedName($domain) - { - try { - $json = $this->master->read($this->getPathForDomain(self::PATH_CACHE_DISTINGUISHED_NAME, $domain)); - - return $this->serializer->deserialize($json, DistinguishedName::class, JsonEncoder::FORMAT); - } catch (\Exception $e) { - throw new AcmeCliException(sprintf('Loading of domain %s distinguished name failed', $domain), $e); - } - } - - /** - * {@inheritdoc} - */ - public function storeDomainCertificate($domain, Certificate $certificate) - { - // Simple certificate - $certPem = $this->serializer->serialize($certificate, PemEncoder::FORMAT); - - // Issuer chain - $issuerChain = []; - $issuerCertificate = $certificate->getIssuerCertificate(); - - while (null !== $issuerCertificate) { - $issuerChain[] = $this->serializer->serialize($issuerCertificate, PemEncoder::FORMAT); - $issuerCertificate = $issuerCertificate->getIssuerCertificate(); - } - - $chainPem = implode("\n", $issuerChain); - - // Full chain - $fullChainPem = $certPem.$chainPem; - - // Combined - $keyPair = $this->loadDomainKeyPair($domain); - $combinedPem = $fullChainPem.$this->serializer->serialize($keyPair->getPrivateKey(), PemEncoder::FORMAT); - - // Save - $this->save($this->getPathForDomain(self::PATH_DOMAIN_CERT_CERT, $domain), $certPem); - $this->save($this->getPathForDomain(self::PATH_DOMAIN_CERT_CHAIN, $domain), $chainPem); - $this->save($this->getPathForDomain(self::PATH_DOMAIN_CERT_FULLCHAIN, $domain), $fullChainPem); - $this->save($this->getPathForDomain(self::PATH_DOMAIN_CERT_COMBINED, $domain), $combinedPem); - } - - /** - * {@inheritdoc} - */ - public function hasDomainCertificate($domain) - { - return $this->master->has($this->getPathForDomain(self::PATH_DOMAIN_CERT_FULLCHAIN, $domain)); - } - - /** - * {@inheritdoc} - */ - public function loadDomainCertificate($domain) - { - try { - $pems = explode('-----BEGIN CERTIFICATE-----', $this->master->read($this->getPathForDomain(self::PATH_DOMAIN_CERT_FULLCHAIN, $domain))); - } catch (\Exception $e) { - throw new AcmeCliException(sprintf('Loading of domain %s certificate failed', $domain), $e); - } - - $pems = array_map(function ($item) { - return trim(str_replace('-----END CERTIFICATE-----', '', $item)); - }, $pems); - array_shift($pems); - $pems = array_reverse($pems); - - $certificate = null; - - foreach ($pems as $pem) { - $certificate = new Certificate( - "-----BEGIN CERTIFICATE-----\n".$pem."\n-----END CERTIFICATE-----", - $certificate - ); - } - - return $certificate; - } - - /** - * {@inheritdoc} - */ - public function storeCertificateOrder(array $domains, CertificateOrder $order) - { - try { - $this->save( - $this->getPathForDomainList(self::PATH_CACHE_CERTIFICATE_ORDER, $domains), - $this->serializer->serialize($order, JsonEncoder::FORMAT) - ); - } catch (\Exception $e) { - throw new AcmeCliException(sprintf('Storing of domains %s certificate order failed', implode(', ', $domains)), $e); - } - } - - /** - * {@inheritdoc} - */ - public function hasCertificateOrder(array $domains) - { - return $this->master->has($this->getPathForDomainList(self::PATH_CACHE_CERTIFICATE_ORDER, $domains)); - } - - /** - * {@inheritdoc} - */ - public function loadCertificateOrder(array $domains) - { - try { - $json = $this->master->read($this->getPathForDomainList(self::PATH_CACHE_CERTIFICATE_ORDER, $domains)); - - return $this->serializer->deserialize($json, CertificateOrder::class, JsonEncoder::FORMAT); - } catch (\Exception $e) { - throw new AcmeCliException(sprintf('Loading of domains %s certificate order failed', implode(', ', $domains)), $e); - } - } - - /** - * {@inheritdoc} - */ - public function save($path, $content, $visibility = self::VISIBILITY_PRIVATE) - { - if (!$this->master->has($path)) { - // File creation: remove from backup if it existed and warm-up both master and backup - $this->createAndBackup($path, $content); - } else { - // File update: backup before writing - $this->backupAndUpdate($path, $content); - } - - if ($this->enableBackup) { - $this->backup->setVisibility($path, $visibility); - } - - $this->master->setVisibility($path, $visibility); - } - - private function createAndBackup($path, $content) - { - if ($this->enableBackup) { - if ($this->backup->has($path)) { - $this->backup->delete($path); - } - - $this->backup->write($path, $content); - } - - $this->master->write($path, $content); - } - - private function backupAndUpdate($path, $content) - { - if ($this->enableBackup) { - $oldContent = $this->master->read($path); - - if (false !== $oldContent) { - if ($this->backup->has($path)) { - $this->backup->update($path, $oldContent); - } else { - $this->backup->write($path, $oldContent); - } - } - } - - $this->master->update($path, $content); - } - - private function normalizeDomain($domain) - { - return $domain; - } - - private function normalizeDomainList(array $domains) - { - $normalizedDomains = array_unique(array_map([$this, 'normalizeDomain'], $domains)); - sort($normalizedDomains); - - return (isset($domains[0]) ? $this->normalizeDomain($domains[0]) : '-').'/'.sha1(json_encode($normalizedDomains)); - } -} diff --git a/AcmePhp/Cli/Repository/RepositoryInterface.php b/AcmePhp/Cli/Repository/RepositoryInterface.php deleted file mode 100644 index f42c5277..00000000 --- a/AcmePhp/Cli/Repository/RepositoryInterface.php +++ /dev/null @@ -1,198 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Repository; - -use AcmePhp\Cli\Exception\AcmeCliException; -use AcmePhp\Core\Protocol\AuthorizationChallenge; -use AcmePhp\Ssl\Certificate; -use AcmePhp\Ssl\CertificateResponse; -use AcmePhp\Ssl\DistinguishedName; -use AcmePhp\Ssl\KeyPair; - -/** - * @author Titouan Galopin - */ -interface RepositoryInterface -{ - const VISIBILITY_PUBLIC = 'public'; - const VISIBILITY_PRIVATE = 'private'; - - /** - * Extract important elements from the given certificate response and store them - * in the repository. - * - * This method will use the distinguished name common name as a domain to store: - * - the key pair - * - the certificate request - * - the certificate - * - * @param CertificateResponse $certificateResponse - * - * @throws AcmeCliException - */ - public function storeCertificateResponse(CertificateResponse $certificateResponse); - - /** - * Store a given key pair as the account key pair (the global key pair used to - * interact with the ACME server). - * - * @param KeyPair $keyPair - * - * @throws AcmeCliException - */ - public function storeAccountKeyPair(KeyPair $keyPair); - - /** - * Check if there is an account key pair in the repository. - * - * @return bool - */ - public function hasAccountKeyPair(); - - /** - * Load the account key pair. - * - * @throws AcmeCliException - * - * @return KeyPair - */ - public function loadAccountKeyPair(); - - /** - * Store a given key pair as associated to a given domain. - * - * @param string $domain - * @param KeyPair $keyPair - * - * @throws AcmeCliException - */ - public function storeDomainKeyPair($domain, KeyPair $keyPair); - - /** - * Check if there is a key pair associated to the given domain in the repository. - * - * @param string $domain - * - * @return bool - */ - public function hasDomainKeyPair($domain); - - /** - * Load the key pair associated to a given domain. - * - * @param string $domain - * - * @throws AcmeCliException - * - * @return KeyPair - */ - public function loadDomainKeyPair($domain); - - /** - * Store a given authorization challenge as associated to a given domain. - * - * @param string $domain - * @param AuthorizationChallenge $authorizationChallenge - * - * @throws AcmeCliException - */ - public function storeDomainAuthorizationChallenge($domain, AuthorizationChallenge $authorizationChallenge); - - /** - * Check if there is an authorization challenge associated to the given domain in the repository. - * - * @param string $domain - * - * @return bool - */ - public function hasDomainAuthorizationChallenge($domain); - - /** - * Load the authorization challenge associated to a given domain. - * - * @param string $domain - * - * @throws AcmeCliException - * - * @return AuthorizationChallenge - */ - public function loadDomainAuthorizationChallenge($domain); - - /** - * Store a given distinguished name as associated to a given domain. - * - * @param string $domain - * @param DistinguishedName $distinguishedName - * - * @throws AcmeCliException - */ - public function storeDomainDistinguishedName($domain, DistinguishedName $distinguishedName); - - /** - * Check if there is a distinguished name associated to the given domain in the repository. - * - * @param string $domain - * - * @return bool - */ - public function hasDomainDistinguishedName($domain); - - /** - * Load the distinguished name associated to a given domain. - * - * @param string $domain - * - * @throws AcmeCliException - * - * @return DistinguishedName - */ - public function loadDomainDistinguishedName($domain); - - /** - * Store a given certificate as associated to a given domain. - * - * @param string $domain - * @param Certificate $certificate - * - * @throws AcmeCliException - */ - public function storeDomainCertificate($domain, Certificate $certificate); - - /** - * Check if there is a certificate associated to the given domain in the repository. - * - * @param string $domain - * - * @return bool - */ - public function hasDomainCertificate($domain); - - /** - * Load the certificate associated to a given domain. - * - * @param string $domain - * - * @throws AcmeCliException - * - * @return Certificate - */ - public function loadDomainCertificate($domain); - - /** - * Save a given string into a given path handling backup. - * - * @param string $path - * @param string $content - * @param string $visibility the visibilty to use for this file - */ - public function save($path, $content, $visibility = self::VISIBILITY_PRIVATE); -} diff --git a/AcmePhp/Cli/Repository/RepositoryV2Interface.php b/AcmePhp/Cli/Repository/RepositoryV2Interface.php deleted file mode 100644 index f3b67e03..00000000 --- a/AcmePhp/Cli/Repository/RepositoryV2Interface.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Repository; - -use AcmePhp\Cli\Exception\AcmeCliException; -use AcmePhp\Core\Protocol\CertificateOrder; - -/** - * @author Titouan Galopin - */ -interface RepositoryV2Interface extends RepositoryInterface -{ - /** - * Store a given certificate as associated to a given domain. - * - * @param array $domains - * @param CertificateOrder $order - * - * @throws AcmeCliException - */ - public function storeCertificateOrder(array $domains, CertificateOrder $order); - - /** - * Check if there is a certificate associated to the given domain in the repository. - * - * @param string $domain - * - * @return bool - */ - public function hasCertificateOrder(array $domains); - - /** - * Load the certificate associated to a given domain. - * - * @param string $domain - * - * @throws AcmeCliException - * - * @return CertificateOrder - */ - public function loadCertificateOrder(array $domains); -} diff --git a/AcmePhp/Cli/Serializer/PemEncoder.php b/AcmePhp/Cli/Serializer/PemEncoder.php deleted file mode 100644 index 01d3a15d..00000000 --- a/AcmePhp/Cli/Serializer/PemEncoder.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Serializer; - -use Symfony\Component\Serializer\Encoder\DecoderInterface; -use Symfony\Component\Serializer\Encoder\EncoderInterface; - -/** - * @author Titouan Galopin - */ -class PemEncoder implements EncoderInterface, DecoderInterface -{ - const FORMAT = 'pem'; - - /** - * {@inheritdoc} - */ - public function encode($data, $format, array $context = []) - { - return trim($data)."\n"; - } - - /** - * {@inheritdoc} - */ - public function decode($data, $format, array $context = []) - { - return trim($data)."\n"; - } - - /** - * {@inheritdoc} - */ - public function supportsEncoding($format) - { - return self::FORMAT === $format; - } - - /** - * {@inheritdoc} - */ - public function supportsDecoding($format) - { - return self::FORMAT === $format; - } -} diff --git a/AcmePhp/Cli/Serializer/PemNormalizer.php b/AcmePhp/Cli/Serializer/PemNormalizer.php deleted file mode 100644 index 564f1b93..00000000 --- a/AcmePhp/Cli/Serializer/PemNormalizer.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Serializer; - -use AcmePhp\Ssl\Certificate; -use AcmePhp\Ssl\Key; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * @author Titouan Galopin - */ -class PemNormalizer implements NormalizerInterface, DenormalizerInterface -{ - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) - { - return $object->getPEM(); - } - - /** - * {@inheritdoc} - */ - public function denormalize($data, $class, $format = null, array $context = []) - { - return new $class($data); - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null) - { - return is_object($data) && ($data instanceof Certificate || $data instanceof Key); - } - - /** - * {@inheritdoc} - */ - public function supportsDenormalization($data, $type, $format = null) - { - return is_string($data); - } -} diff --git a/README.md b/README.md index fd54319a..0e59a1a8 100644 --- a/README.md +++ b/README.md @@ -3,28 +3,23 @@ Performs basic site functions in easyengine. `site` command contains following subcommand - * [create](#ee-site-create) - * [delete](#ee-site-delete) - * [disable](#ee-site-disable) - * [enable](#ee-site-enable) - * [info](#ee-site-info) - * [list](#ee-site-list) - * [start](#ee-site-start) - * [stop](#ee-site-stop) - * [restart](#ee-site-restart) - * [reload](#ee-site-reload) + * [create](#create) + * [delete](#delete) + * [disable](#disable) + * [enable](#enable) + * [info](#info) + * [list](#list) + * [up](#up) + * [down](#down) + * [restart](#restart) + * [reload](#reload) ## create Runs the site creation. - +Check [this](https://github.com/EasyEngine/site-wp-command) for `--type=wp` support package. ```bash -ee site create example.com # install wordpress without any page caching (default) -ee site create example.com --wp # install wordpress without any page caching -ee site create example.com --wpredis # install wordpress with page caching -ee site create example.com --wpsubir # install wpmu-subdirectory without any page caching -ee site create example.com --wpsubir --wpredis # install wpmu-subdirectory with page caching -ee site create example.com --wpsubdom # install wpmu-subdomain without any page caching -ee site create example.com --wpsubdom --wpredis # install wpmu-subdomain with page cache +ee site create example.com # install html site (default) +ee site create example.com --type=wp # install wp site ``` Let's Encrypt SSL @@ -32,8 +27,6 @@ Let's Encrypt SSL # Enable SSL using Let’s Encrypt (You can add --letsencrypt along with any other flag.) ee site create example.com [--letsencrypt|--le] ee site create example.com --le # install wordpress without any page caching + letsencrypt ssl -ee site create example.com --wpredis --le # install wordpress with page caching + letsencrypt ssl -ee site create example.com --wpsubdom --le # install wordpress wpmu-subdomain + wildcard letsencrypt ssl ``` ## delete @@ -75,20 +68,20 @@ ee site list --enabled # List enabled sites ee site list --disabled # List disabled sites ``` -## start +## up Starts services associated with site. ```bash -ee site start example.com # Defaults to all services -ee site start example.com --nginx +ee site up example.com # Defaults to all services +ee site up example.com --nginx ``` -## stop +## down Stops services associated with site. ```bash -ee site stop example.com # Defaults to all services -ee site stop example.com --mailhog +ee site down example.com # Defaults to all services +ee site down example.com --mailhog ``` ## restart diff --git a/composer.json b/composer.json index cb29b74f..8faa7653 100644 --- a/composer.json +++ b/composer.json @@ -25,13 +25,5 @@ "site list", "site delete" ] - }, - "require": { - "acmephp/core": "dev-master", - "ext-openssl": "*", - "guzzlehttp/guzzle": "^6.0", - "league/flysystem": "^1.0.19", - "symfony/serializer": "^3.0", - "webmozart/assert": "^1.0" } } diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 50ea7f57..bec661c4 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -1,10 +1,48 @@ init_logger(); +/* End. Loading required files to enable EE::launch() in tests. */ + use Behat\Behat\Context\Context; use Behat\Behat\Hook\Scope\AfterFeatureScope; use Behat\Behat\Hook\Scope\AfterScenarioScope; @@ -344,7 +382,7 @@ public function thereShouldBeContainersWithLabel($expected_running_containers, P $labels = $pyStringNode->getStrings(); $label_string = implode($labels, ' -f label='); - $result = EE::launch( "docker ps -aqf label=$label_string | wc -l", false, true ); + $result = EE::launch( "docker ps -qf label=$label_string | wc -l", false, true ); $running_containers = (int) trim($result->stdout); if($expected_running_containers === $running_containers) { diff --git a/features/labels.feature b/features/labels.feature index c67f188e..25ff2184 100644 --- a/features/labels.feature +++ b/features/labels.feature @@ -3,7 +3,7 @@ Feature: Container Labels Scenario: All easyengine containers are tagged Given I run "bin/ee site create labels.test" - Then There should be 5 containers with labels + Then There should be 1 containers with labels """ io.easyengine.site=labels.test """ diff --git a/features/redirect.feature b/features/redirect.feature index ad37dcad..273e3dbb 100644 --- a/features/redirect.feature +++ b/features/redirect.feature @@ -1,14 +1,16 @@ Feature: Site Redirection Scenario: no_www-no_ssl redirection works properly - When I run 'sudo bin/ee site create example.test' + When I run 'bin/ee site create example.test' + Then After delay of 5 seconds Then Request on 'localhost' with header 'Host: www.example.test' should contain following headers: | header | | HTTP/1.1 301 Moved Permanently | | Location: http://example.test/ | Scenario: www-no_ssl redirection works properly - When I run 'sudo bin/ee site create www.example1.test' + When I run 'bin/ee site create www.example1.test' + Then After delay of 5 seconds Then Request on 'localhost' with header 'Host: example1.test' should contain following headers: | header | | HTTP/1.1 301 Moved Permanently | @@ -16,22 +18,24 @@ Feature: Site Redirection Scenario: no_www-ssl redirection works properly When I run 'sudo bin/ee site create example2.test --le --le-mail=test@test.com --skip-status-check' + Then After delay of 5 seconds Then Request on 'localhost' with header 'Host: www.example2.test' should contain following headers: - | header | - | HTTP/1.1 301 Moved Permanently | - | Location: https://example2.test/ | + | header | + | HTTP/1.1 301 Moved Permanently | + | Location: https://example2.test/ | And Request on 'https://www.example2.test' with resolve option 'www.example2.test:443:127.0.0.1' should contain following headers: - | header | - | HTTP/1.1 301 Moved Permanently | - | Location: https://example2.test/ | + | header | + | HTTP/1.1 301 Moved Permanently | + | Location: https://example2.test/ | Scenario: www-ssl redirection works properly When I run 'sudo bin/ee site create www.example3.test --le --le-mail=test@test.com --skip-status-check' + Then After delay of 5 seconds Then Request on 'localhost' with header 'Host: example3.test' should contain following headers: - | header | - | HTTP/1.1 301 Moved Permanently | - | Location: https://www.example3.test/ | + | header | + | HTTP/1.1 301 Moved Permanently | + | Location: https://www.example3.test/ | And Request on 'https://example3.test/' with resolve option 'example3.test:443:127.0.0.1' should contain following headers: - | header | - | HTTP/1.1 301 Moved Permanently | - | Location: https://www.example3.test/ | + | header | + | HTTP/1.1 301 Moved Permanently | + | Location: https://www.example3.test/ | diff --git a/features/site.feature b/features/site.feature index a8f54279..827f6ed8 100644 --- a/features/site.feature +++ b/features/site.feature @@ -1,16 +1,8 @@ Feature: Site Command - Scenario: ee throws error when run without root - Given 'bin/ee' is installed - When I run 'bin/ee' - Then STDERR should return exactly - """ - Error: Please run `ee` with root privileges. - """ - Scenario: ee executable is command working correctly Given 'bin/ee' is installed - When I run 'sudo bin/ee' + When I run 'bin/ee' Then STDOUT should return something like """ NAME @@ -19,64 +11,37 @@ Feature: Site Command """ Scenario: Check site command is present - When I run 'sudo bin/ee site' + When I run 'bin/ee site' Then STDOUT should return something like """ usage: ee site """ - Scenario: Create wp site successfully - When I run 'sudo bin/ee site create wp.test --wp' - Then The site 'wp.test' should have webroot - And The site 'wp.test' should have WordPress - And Request on 'wp.test' should contain following headers: - | header | - | HTTP/1.1 200 OK | - - Scenario: Create wpsubdir site successfully - When I run 'sudo bin/ee site create wpsubdir.test --wpsubdir' - And I create subsite '1' in 'wpsubdir.test' - Then The site 'wpsubdir.test' should have webroot - And The site 'wpsubdir.test' should have WordPress - And The site 'wpsubdir.test' should be 'subdir' multisite - And Request on 'wpsubdir.test' should contain following headers: - | header | - | HTTP/1.1 200 OK | - - Scenario: Create wpsubdom site successfully - When I run 'sudo bin/ee site create wpsubdom.test --wpsubdom' - And I create subsite '1' in 'wpsubdom.test' - Then The site 'wpsubdom.test' should have webroot - And The site 'wpsubdom.test' should have WordPress - And The site 'wpsubdom.test' should be 'subdomain' multisite - And Request on 'wpsubdom.test' should contain following headers: + Scenario: Create html site successfully + When I run 'bin/ee site create site.test --type=html' + Then The site 'site.test' should have webroot + And Request on 'site.test' should contain following headers: | header | | HTTP/1.1 200 OK | Scenario: List the sites - When I run 'sudo bin/ee site list --format=text' + When I run 'bin/ee site list --format=text' Then STDOUT should return exactly """ - wp.test - wpsubdir.test - wpsubdom.test + site.test """ Scenario: Delete the sites - When I run 'sudo bin/ee site delete wp.test --yes' + When I run 'bin/ee site delete site.test --yes' Then STDOUT should return something like """ - Site wp.test deleted. + Site site.test deleted. """ And STDERR should return exactly """ """ - And The 'wp.test' db entry should be removed - And The 'wp.test' webroot should be removed - And Following containers of site 'wp.test' should be removed: + And The 'site.test' db entry should be removed + And The 'site.test' webroot should be removed + And Following containers of site 'site.test' should be removed: | container | | nginx | - | php | - | db | - | redis | - | phpmyadmin | diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 00000000..34d60459 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,43 @@ + + + WordPress Coding Standards for EE + + + + + + + + + + + . + */ci/* + */features/* + */packages/* + */tests/* + */utils/* + */vendor/* + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Shutdown_Handler.php b/src/Shutdown_Handler.php deleted file mode 100644 index 2df3c30e..00000000 --- a/src/Shutdown_Handler.php +++ /dev/null @@ -1,19 +0,0 @@ -getMethod( 'shutDownFunction' ); - $method->setAccessible( true ); - $method->invoke( $site_command[0] ); - } -} diff --git a/src/Site_Command.php b/src/Site_Command.php index 6fc52948..a78f2976 100644 --- a/src/Site_Command.php +++ b/src/Site_Command.php @@ -3,44 +3,69 @@ declare( ticks=1 ); /** - * Creates a simple WordPress Website. + * Creates a simple html Website. * * ## EXAMPLES * - * # Create simple WordPress site - * $ ee site create example.com --wp + * # Create simple html site + * $ ee site create example.com * * @package ee-cli */ -class Site_Command extends EE_Command { - private $site_name; - private $site_root; - private $site_type; - private $site_title; - private $site_user; - private $site_pass; - private $site_email; - private $proxy_type; - private $cache_type; - private $db; + +use \Symfony\Component\Filesystem\Filesystem; + +class Site_Command extends EE_Site_Command { + + /** + * @var string $command Name of the command being run. + */ + private $command; + + /** + * @var array $site Associative array containing essential site related information. + */ + private $site; + + /** + * @var object $docker Object to access `EE::docker()` functions. + */ private $docker; + + /** + * @var int $level The level of creation in progress. Essential for rollback in case of failure. + */ private $level; + + /** + * @var object $logger Object of logger. + */ private $logger; + + /** + * @var bool $le Whether the site is letsencrypt or not. + */ private $le; - private $db_name; - private $db_user; - private $db_root_pass; - private $db_pass; - private $db_host; - private $db_port; - private $locale; - private $skip_install; + + /** + * @var bool $skip_chk To skip site status check pre-installation. + */ private $skip_chk; - private $force; - private $le_mail; + + /** + * @var Filesystem $fs Symfony Filesystem object. + */ + private $fs; + + /** + * @var Object $db Object to access `EE::db()` functions. + */ + private $db; public function __construct() { - $this->level = 0; + + $this->level = 0; + $this->command = 'site'; pcntl_signal( SIGTERM, [ $this, "rollback" ] ); pcntl_signal( SIGHUP, [ $this, "rollback" ] ); pcntl_signal( SIGUSR1, [ $this, "rollback" ] ); @@ -50,6 +75,7 @@ public function __construct() { $this->db = EE::db(); $this->docker = EE::docker(); $this->logger = EE::get_file_logger()->withName( 'site_command' ); + $this->fs = new Filesystem(); } /** @@ -60,326 +86,40 @@ public function __construct() { * * : Name of website. * - * [--wp] - * : WordPress website. - * - * [--wpredis] - * : Use redis for WordPress. - * - * [--wpsubdir] - * : WordPress sub-dir Multi-site. - * - * [--wpsubdom] - * : WordPress sub-domain Multi-site. - * - * [--title=] - * : Title of your site. - * - * [--admin_user=<admin_user>] - * : Username of the administrator. - * - * [--admin_pass=<admin_pass>] - * : Password for the the administrator. - * - * [--admin_email=<admin_email>] - * : E-Mail of the administrator. - * - * [--dbname=<dbname>] - * : Set the database name. - * --- - * default: wordpress - * --- - * - * [--dbuser=<dbuser>] - * : Set the database user. - * - * [--dbpass=<dbpass>] - * : Set the database password. - * - * [--dbhost=<dbhost>] - * : Set the database host. Pass value only when remote dbhost is required. - * --- - * default: db - * --- - * - * [--dbprefix=<dbprefix>] - * : Set the database table prefix. - * - * [--dbcharset=<dbcharset>] - * : Set the database charset. - * --- - * default: utf8 - * --- - * - * [--dbcollate=<dbcollate>] - * : Set the database collation. - * - * [--skip-check] - * : If set, the database connection is not checked. - * - * [--version=<version>] - * : Select which wordpress version you want to download. Accepts a version number, ‘latest’ or ‘nightly’. - * - * [--skip-content] - * : Download WP without the default themes and plugins. + * [--letsencrypt] + * : Enables ssl via letsencrypt certificate. * - * [--skip-install] - * : Skips wp-core install. + * [--type=<type>] + * : Type of the site to be created. Values: html,php,wp. * * [--skip-status-check] * : Skips site status check. - - * [--letsencrypt] - * : Enables ssl via letsencrypt certificate. - * - * [--force] - * : Resets the remote database if it is not empty. */ public function create( $args, $assoc_args ) { - \EE\Utils\delem_log( 'site create start' ); + EE\Utils\delem_log( 'site create start' ); EE::warning( 'This is a beta version. Please don\'t use it in production.' ); $this->logger->debug( 'args:', $args ); - $this->logger->debug( 'assoc_args:', empty( $assoc_args ) ? array( 'NULL' ) : $assoc_args ); - $this->site_name = strtolower( \EE\Utils\remove_trailing_slash( $args[0] ) ); - $this->site_type = \EE\Utils\get_type( $assoc_args, [ 'wp', 'wpsubdom', 'wpsubdir' ], 'wp' ); - if ( false === $this->site_type ) { - EE::error( 'Invalid arguments' ); + $this->logger->debug( 'assoc_args:', empty( $assoc_args ) ? [ 'NULL' ] : $assoc_args ); + $this->site['name'] = strtolower( EE\Utils\remove_trailing_slash( $args[0] ) ); + $this->site['type'] = EE\Utils\get_flag_value( $assoc_args, 'type', 'html' ); + if ( 'html' !== $this->site['type'] ) { + EE::error( sprintf( 'Invalid site-type: %s', $this->site['type'] ) ); } - if ( $this->db::site_in_db( $this->site_name ) ) { - EE::error( "Site $this->site_name already exists. If you want to re-create it please delete the older one using:\n`ee site delete $this->site_name`" ); - } - - $this->proxy_type = 'ee-nginx-proxy'; - $this->cache_type = ! empty( $assoc_args['wpredis'] ) ? 'wpredis' : 'none'; - $this->le = \EE\Utils\get_flag_value( $assoc_args, 'letsencrypt' ); - $this->site_title = \EE\Utils\get_flag_value( $assoc_args, 'title', $this->site_name ); - $this->site_user = \EE\Utils\get_flag_value( $assoc_args, 'admin_user', 'admin' ); - $this->site_pass = \EE\Utils\get_flag_value( $assoc_args, 'admin_pass', \EE\Utils\random_password() ); - $this->db_name = str_replace( [ '.', '-' ], '_', $this->site_name ); - $this->db_host = \EE\Utils\get_flag_value( $assoc_args, 'dbhost' ); - $this->db_user = \EE\Utils\get_flag_value( $assoc_args, 'dbuser', 'wordpress' ); - $this->db_pass = \EE\Utils\get_flag_value( $assoc_args, 'dbpass', \EE\Utils\random_password() ); - $this->locale = \EE\Utils\get_flag_value( $assoc_args, 'locale', EE::get_config( 'locale' ) ); - $this->db_root_pass = \EE\Utils\random_password(); - - // If user wants to connect to remote database - if ( 'db' !== $this->db_host ) { - if ( ! isset( $assoc_args['dbuser'] ) || ! isset( $assoc_args['dbpass'] ) ) { - EE::error( '`--dbuser` and `--dbpass` are required for remote db host.' ); - } - $arg_host_port = explode( ':', $this->db_host ); - $this->db_host = $arg_host_port[0]; - $this->db_port = empty( $arg_host_port[1] ) ? '3306' : $arg_host_port[1]; + if ( $this->db::site_in_db( $this->site['name'] ) ) { + EE::error( sprintf( "Site %1\$s already exists. If you want to re-create it please delete the older one using:\n`ee site delete %1\$s`", $this->site['name'] ) ); } - $this->site_email = \EE\Utils\get_flag_value( $assoc_args, 'admin_email', strtolower( 'mail@' . $this->site_name ) ); - $this->skip_install = \EE\Utils\get_flag_value( $assoc_args, 'skip-install' ); - $this->skip_chk = \EE\Utils\get_flag_value( $assoc_args, 'skip-status-check' ); - $this->force = \EE\Utils\get_flag_value( $assoc_args, 'force' ); + $this->le = EE\Utils\get_flag_value( $assoc_args, 'letsencrypt' ); + $this->skip_chk = EE\Utils\get_flag_value( $assoc_args, 'skip-status-check' ); - $this->init_checks(); + EE\SiteUtils\init_checks(); EE::log( 'Configuring project.' ); - $this->create_site( $assoc_args ); - \EE\Utils\delem_log( 'site create end' ); - } - - /** - * Lists the created websites. - * - * [--enabled] - * : List only enabled sites. - * - * [--disabled] - * : List only disabled sites. - * - * [--format=<format>] - * : Render output in a particular format. - * --- - * default: table - * options: - * - table - * - csv - * - yaml - * - json - * - count - * - text - * --- - * - * @subcommand list - */ - public function _list( $args, $assoc_args ) { - \EE\Utils\delem_log( 'site list start' ); - - $format = \EE\Utils\get_flag_value( $assoc_args, 'format' ); - $enabled = \EE\Utils\get_flag_value( $assoc_args, 'enabled' ); - $disabled = \EE\Utils\get_flag_value( $assoc_args, 'disabled' ); - - $where = array(); - - if ( $enabled && ! $disabled ) { - $where['is_enabled'] = 1; - } elseif ( $disabled && ! $enabled ) { - $where['is_enabled'] = 0; - } - - $sites = $this->db::select( array( 'sitename', 'is_enabled' ), $where ); - - if ( ! $sites ) { - EE::error( 'No sites found!' ); - } - - if ( 'text' === $format ) { - foreach ( $sites as $site ) { - EE::log( $site['sitename'] ); - } - } else { - $result = array_map( - function ( $site ) { - $site['site'] = $site['sitename']; - $site['status'] = $site['is_enabled'] ? 'enabled' : 'disabled'; - - return $site; - }, $sites - ); - - $formatter = new \EE\Formatter( $assoc_args, [ 'site', 'status' ] ); - - $formatter->display_items( $result ); - } - - \EE\Utils\delem_log( 'site list end' ); - } - - /** - * Deletes a website. - * - * ## OPTIONS - * - * <site-name> - * : Name of website to be deleted. - * - * [--yes] - * : Do not prompt for confirmation. - */ - public function delete( $args, $assoc_args ) { - \EE\Utils\delem_log( 'site delete start' ); - $this->populate_site_info( $args ); - EE::confirm( "Are you sure you want to delete $this->site_name?", $assoc_args ); - $this->level = 5; - $this->delete_site(); - \EE\Utils\delem_log( 'site delete end' ); - } - - - /** - * Runs the acme le registration and authorization. - */ - private function init_le() { - $client = new Site_Letsencrypt(); - $this->le_mail = EE::get_runner()->config[ 'le-mail' ] ?? EE::input( 'Enter your mail id: ' ); - EE::get_runner()->ensure_present_in_config( 'le-mail', $this->le_mail ); - if ( ! $client->register( $this->le_mail ) ) { - $this->le = false; - - return; - } - $wildcard = 'wpsubdom' === $this->site_type ? true : false; - $domains = $wildcard ? [ "*.$this->site_name", $this->site_name ] : [ $this->site_name ]; - if ( ! $client->authorize( $domains, $this->site_root, $wildcard ) ) { - $this->le = false; - - return; - } - if ( $wildcard ) { - echo \cli\Colors::colorize( "%YIMPORTANT:%n Run `ee site le $this->site_name` once the dns changes have propogated to complete the certification generation and installation.", null ); - } else { - $this->le(); - } - } - - - /** - * Runs the acme le. - * - * ## OPTIONS - * - * <site-name> - * : Name of website. - * - * [--force] - * : Force renewal. - */ - public function le( $args = [], $assoc_args = [] ) { - if ( ! isset( $this->site_name ) ) { - $this->populate_site_info( $args ); - } - if ( ! isset( $this->le_mail ) ) { - $this->le_mail = EE::get_config( 'le-mail' ) ?? EE::input( 'Enter your mail id: ' ); - } - $force = \EE\Utils\get_flag_value( $assoc_args, 'force' ); - $wildcard = 'wpsubdom' === $this->site_type ? true : false; - $domains = $wildcard ? [ "*.$this->site_name", $this->site_name ] : [ $this->site_name ]; - $client = new Site_Letsencrypt(); - if ( ! $client->check( $domains, $wildcard ) ) { - $this->le = false; - - return; - } - if ( $wildcard ) { - $client->request( "*.$this->site_name", [ $this->site_name ], $this->le_mail, $force ); - } else { - $client->request( $this->site_name, [], $this->le_mail, $force ); - $client->cleanup( $this->site_root ); - } - EE::launch( 'docker exec ee-nginx-proxy sh -c "/app/docker-entrypoint.sh /usr/local/bin/docker-gen /app/nginx.tmpl /etc/nginx/conf.d/default.conf; /usr/sbin/nginx -s reload"' ); - } - - /** - * Enables a website. It will start the docker containers of the website if they are stopped. - * - * ## OPTIONS - * - * [<site-name>] - * : Name of website to be enabled. - */ - public function enable( $args ) { - \EE\Utils\delem_log( 'site enable start' ); - $args = \EE\Utils\set_site_arg( $args, 'site enable' ); - $this->populate_site_info( $args ); - EE::log( "Enabling site $this->site_name." ); - if ( $this->docker::docker_compose_up( $this->site_root ) ) { - $this->db::update( [ 'is_enabled' => '1' ], [ 'sitename' => $this->site_name ] ); - EE::success( "Site $this->site_name enabled." ); - } else { - EE::error( "There was error in enabling $this->site_name. Please check logs." ); - } - \EE\Utils\delem_log( 'site enable end' ); - } - - /** - * Disables a website. It will stop and remove the docker containers of the website if they are running. - * - * ## OPTIONS - * - * [<site-name>] - * : Name of website to be disabled. - */ - public function disable( $args ) { - \EE\Utils\delem_log( 'site disable start' ); - $args = \EE\Utils\set_site_arg( $args, 'site disable' ); - $this->populate_site_info( $args ); - EE::log( "Disabling site $this->site_name." ); - if ( $this->docker::docker_compose_down( $this->site_root ) ) { - $this->db::update( [ 'is_enabled' => '0' ], [ 'sitename' => $this->site_name ] ); - EE::success( "Site $this->site_name disabled." ); - } else { - EE::error( "There was error in disabling $this->site_name. Please check logs." ); - } - \EE\Utils\delem_log( 'site disable end' ); + $this->create_site(); + EE\Utils\delem_log( 'site create end' ); } /** @@ -388,818 +128,125 @@ public function disable( $args ) { * [<site-name>] * : Name of the website whose info is required. */ - public function info( $args ) { - \EE\Utils\delem_log( 'site info start' ); - if ( ! isset( $this->site_name ) ) { - $args = \EE\Utils\set_site_arg( $args, 'site info' ); + public function info( $args, $assoc_args ) { + + EE\Utils\delem_log( 'site info start' ); + if ( ! isset( $this->site['name'] ) ) { + $args = EE\SiteUtils\auto_site_name( $args, $this->command, __FUNCTION__ ); $this->populate_site_info( $args ); } - $ssl = $this->le ? 'Enabled' : 'Not Enabled'; - EE::log( "Details for site $this->site_name:" ); + $ssl = $this->le ? 'Enabled' : 'Not Enabled'; $prefix = ( $this->le ) ? 'https://' : 'http://'; - $info = array( - array( 'Site', $prefix . $this->site_name ), - array( 'Access phpMyAdmin', $prefix . $this->site_name . '/ee-admin/pma/' ), - array( 'Access mailhog', $prefix . $this->site_name . '/ee-admin/mailhog/' ), - array( 'Site Title', $this->site_title ), - array( 'DB Root Password', $this->db_root_pass ), - array( 'DB Name', $this->db_name ), - array( 'DB User', $this->db_user ), - array( 'DB Password', $this->db_pass ), - array( 'E-Mail', $this->site_email ), - array( 'Cache Type', $this->cache_type ), - array( 'SSL', $ssl ), - ); - - if ( ! empty( $this->site_user ) && ! $this->skip_install ) { - $info[] = array( 'WordPress Username', $this->site_user ); - $info[] = array( 'WordPress Password', $this->site_pass ); - } - - \EE\Utils\format_table( $info ); - - \EE\Utils\delem_log( 'site info end' ); - } - - /** - * Starts containers associated with site. - * When no service(--nginx etc.) is specified, all containers will be restarted. - * - * <site-name> - * : Name of the site. - * - * [--all] - * : Start all containers of site. - * - * [--nginx] - * : Start nginx container of site. - * - * [--php] - * : Start php container of site. - * - * [--mysql] - * : Start mysql container of site. - * - * [--redis] - * : Start redis container of site. - * - * [--mailhog] - * : Start mailhog container of site. - * - * [--phpmyadmin] - * : Start phpmyadmin container of site. - * - * [--phpredisadmin] - * : Start phpredisadmin container of site. - * - * [--adminer] - * : Start adminer container of site. - * - * [--anemometer] - * : Start anemometer container of site. - */ - public function start( $args, $assoc_args ) { - $this->site_docker_compose_execute( $args[0], 'start', $args, $assoc_args); - } - - /** - * Stops containers associated with site. - * When no service(--nginx etc.) is specified, all containers will be stopped. - * - * <site-name> - * : Name of the site. - * - * [--all] - * : Stop all containers of site. - * - * [--nginx] - * : Stop nginx container of site. - * - * [--php] - * : Stop php container of site. - * - * [--mysql] - * : Stop mysql container of site. - * - * [--redis] - * : Stop redis container of site. - * - * [--mailhog] - * : Stop mailhog container of site. - * - * [--phpmyadmin] - * : Stop phpmyadmin container of site. - * - * [--phpredisadmin] - * : Stop phpredisadmin container of site. - * - * [--adminer] - * : Stop adminer container of site. - * - * [--anemometer] - * : Stop anemometer container of site. - */ - public function stop( $args, $assoc_args ) { - $this->site_docker_compose_execute( $args[0], 'stop', $args, $assoc_args); - } - - /** - * Restarts containers associated with site. - * When no service(--nginx etc.) is specified, all containers will be restarted. - * - * <site-name> - * : Name of the site. - * - * [--all] - * : Restart all containers of site. - * - * [--nginx] - * : Restart nginx container of site. - * - * [--php] - * : Restart php container of site. - * - * [--mysql] - * : Restart mysql container of site. - * - * [--redis] - * : Restart redis container of site. - * - * [--mailhog] - * : Restart mailhog container of site. - * - * [--phpmyadmin] - * : Restart phpmyadmin container of site. - * - * [--phpredisadmin] - * : Restart phpredisadmin container of site. - * - * [--adminer] - * : Restart adminer container of site. - * - * [--anemometer] - * : Restart anemometer container of site. - */ - public function restart( $args, $assoc_args ) { - $this->site_docker_compose_execute( $args[0], 'restart', $args, $assoc_args); - } - - /** - * Reload services in containers without restarting container(s) associated with site. - * When no service(--nginx etc.) is specified, all services will be reloaded. - * - * <site-name> - * : Name of the site. - * - * [--all] - * : Reload all services of site(which are supported). - * - * [--nginx] - * : Reload nginx service in container. - * - * [--php] - * : Start php service in container. - */ - public function reload( $args, $assoc_args ) { - $this->site_docker_compose_execute( $args[0], 'reload', $args, $assoc_args); - } - - private function site_docker_compose_execute( $site, $action, $args, $assoc_args ) { - $all = \EE\Utils\get_flag_value( $assoc_args, 'all' ); - $no_service_specified = count( $assoc_args ) === 0 ; - - $this->populate_site_info( $args ); - - chdir( $this->site_root ); - - if( $all || $no_service_specified ) { - if( $action === 'reload' ) { - $this->reload_services( [ 'nginx', 'php' ] ); - return; - } - $this->run_compose_command( $action, '', null, 'all services' ); - } - else { - $services = array_map( [$this, 'map_args_to_service'], array_keys( $assoc_args ) ); - - if( $action === 'reload' ) { - $this->reload_services( $services ); - return; - } - - foreach( $services as $service ) { - $this->run_compose_command( $action, $service ); - } - } - } - - - /** - * Generic function to run a docker compose command. Must be ran inside correct directory. - */ - private function run_compose_command( $action, $container, $action_to_display = null, $service_to_display = null) { - $display_action = $action_to_display ? $action_to_display : $action; - $display_service = $service_to_display ? $service_to_display : $container; - - \EE::log( ucfirst( $display_action ) . 'ing ' . $display_service ); - \EE\Utils\default_launch( "docker-compose $action $container" ); - } - - /** - * Executes reload commands. It needs seperate handling as commands to reload each service is different. - */ - private function reload_services( $services ) { - $reload_command = [ - 'nginx' => 'nginx sh -c \'nginx -t && service openresty reload\'', - 'php' => 'php kill -USR2 1' + $info = [ + [ 'Site', $prefix . $this->site['name'] ], + [ 'Site Root', $this->site['root'] ], + [ 'SSL', $ssl ], ]; - foreach( $services as $service ) { - $this->run_compose_command( 'exec', $reload_command[ $service ], 'reload', $service ); - } - } + EE\Utils\format_table( $info ); - /** - * Maps argument passed from cli to docker-compose service name - */ - private function map_args_to_service( $arg ) { - $services_map = [ - 'mysql' => 'db', - ]; - return in_array( $arg, array_keys( $services_map ) ) ? $services_map[ $arg ] : $arg ; + EE\Utils\delem_log( 'site info end' ); } - /** - * Function to check all the required configurations needed to create the site. - * - * Boots up the container if it is stopped or not running. - */ - private function init_checks() { - if ( 'running' !== $this->docker::container_status( $this->proxy_type ) ) { - /** - * Checking ports. - */ - @fsockopen( 'localhost', 80, $port_80_exit_status ); - @fsockopen( 'localhost', 443, $port_443_exit_status ); - - // if any/both the port/s is/are occupied. - if ( ! ( $port_80_exit_status && $port_443_exit_status ) ) { - EE::error( 'Cannot create/start proxy container. Please make sure port 80 and 443 are free.' ); - } else { - $EE_CONF_ROOT = EE_CONF_ROOT; - $ee_proxy_command = "docker run --name $this->proxy_type -e LOCAL_USER_ID=`id -u` -e LOCAL_GROUP_ID=`id -g` --restart=always -d -p 80:80 -p 443:443 -v $EE_CONF_ROOT/nginx/certs:/etc/nginx/certs -v $EE_CONF_ROOT/nginx/dhparam:/etc/nginx/dhparam -v $EE_CONF_ROOT/nginx/conf.d:/etc/nginx/conf.d -v $EE_CONF_ROOT/nginx/htpasswd:/etc/nginx/htpasswd -v $EE_CONF_ROOT/nginx/vhost.d:/etc/nginx/vhost.d -v /var/run/docker.sock:/tmp/docker.sock:ro -v $EE_CONF_ROOT:/app/ee4 -v /usr/share/nginx/html easyengine/nginx-proxy:v" . EE_VERSION; - - - if ( $this->docker::boot_container( $this->proxy_type, $ee_proxy_command ) ) { - EE::success( "$this->proxy_type container is up." ); - } else { - EE::error( "There was some error in starting $this->proxy_type container. Please check logs." ); - } - } - } - - $this->site_root = WEBROOT . $this->site_name; - $this->create_site_root(); - } /** * Function to configure site and copy all the required files. */ - private function configure_site() { + private function configure_site_files() { - $site_conf_dir = $this->site_root . '/config'; - $site_docker_yml = $this->site_root . '/docker-compose.yml'; - $site_conf_env = $this->site_root . '/.env'; + $site_conf_dir = $this->site['root'] . '/config'; + $site_docker_yml = $this->site['root'] . '/docker-compose.yml'; + $site_conf_env = $this->site['root'] . '/.env'; $site_nginx_default_conf = $site_conf_dir . '/nginx/default.conf'; - $site_php_ini = $site_conf_dir . '/php-fpm/php.ini'; - $server_name = ( 'wpsubdom' === $this->site_type ) ? "$this->site_name *.$this->site_name" : $this->site_name; + $site_src_dir = $this->site['root'] . '/app/src'; $process_user = posix_getpwuid( posix_geteuid() ); - EE::log( "Creating WordPress site $this->site_name." ); + EE::log( sprintf( 'Creating site %s.', $this->site['name'] ) ); EE::log( 'Copying configuration files.' ); - $filter = array(); - $filter[] = $this->site_type; - $filter[] = $this->cache_type; + $filter = []; + $filter[] = $this->site['type']; $filter[] = $this->le; - $filter[] = $this->db_host; $site_docker = new Site_Docker(); $docker_compose_content = $site_docker->generate_docker_compose_yml( $filter ); - $default_conf_content = $this->generate_default_conf( $this->site_type, $this->cache_type, $server_name ); - $local = ( 'db' === $this->db_host ) ? true : false; - $env_data = [ - 'local' => $local, - 'virtual_host' => $this->site_name, - 'root_password' => $this->db_root_pass, - 'database_name' => $this->db_name, - 'database_user' => $this->db_user, - 'user_password' => $this->db_pass, - 'wp_db_host' => "$this->db_host:$this->db_port", - 'wp_db_user' => $this->db_user, - 'wp_db_name' => $this->db_name, - 'wp_db_pass' => $this->db_pass, - 'user_id' => $process_user['uid'], - 'group_id' => $process_user['gid'], - ]; - $env_content = \EE\Utils\mustache_render( SITE_TEMPLATE_ROOT . '/config/.env.mustache', $env_data ); - $php_ini_content = \EE\Utils\mustache_render( SITE_TEMPLATE_ROOT . '/config/php-fpm/php.ini.mustache', [] ); - - $this->add_site_redirects(); - - try { - if ( ! ( file_put_contents( $site_docker_yml, $docker_compose_content ) - && file_put_contents( $site_conf_env, $env_content ) - && mkdir( $site_conf_dir ) - && mkdir( $site_conf_dir . '/nginx' ) - && file_put_contents( $site_nginx_default_conf, $default_conf_content ) - && mkdir( $site_conf_dir . '/php-fpm' ) - && file_put_contents( $site_php_ini, $php_ini_content ) ) ) { - throw new Exception( 'Could not copy configuration files.' ); - } - EE::success( 'Configuration files copied.' ); - } - catch ( Exception $e ) { - $this->catch_clean( $e ); - } - } - - /** - * Adds www to non-www redirection to site - */ - private function add_site_redirects() { - $confd_path = EE_CONF_ROOT . '/nginx/conf.d/'; - $config_file_path = $confd_path . $this->site_name . '-redirect.conf'; - $has_www = strpos( $this->site_name, 'www.' ) === 0; - $content = ''; - - if( $has_www ) { - $site_name_without_www = ltrim( $this->site_name, '.www' ); - // ee site create www.example.com --le - if( $this->le ) { - $content = " -server { - listen 80; - listen 443; - server_name $site_name_without_www; - return 301 https://$this->site_name\$request_uri; -}"; - } - // ee site create www.example.com - else { - $content = " -server { - listen 80; - server_name $site_name_without_www; - return 301 http://$this->site_name\$request_uri; -}"; - } - } - else { - $site_name_with_www = 'www.' . $this->site_name; - // ee site create example.com --le - if( $this->le ) { - - $content = " -server { - listen 80; - listen 443; - server_name $site_name_with_www; - return 301 https://$this->site_name\$request_uri; -}"; - } - // ee site create example.com - else { - $content = " -server { - listen 80; - server_name $site_name_with_www; - return 301 http://$this->site_name\$request_uri; -}"; - } - } - file_put_contents( $config_file_path, ltrim( $content, PHP_EOL ) ); - } - - /** - * Function to generate default.conf from mustache templates. - * - * @param string $site_type Type of site (wpsubdom, wpredis etc..) - * @param string $cache_type Type of cache(wpredis or none) - * @param string $server_name Name of server to use in virtual_host - */ - private function generate_default_conf( $site_type, $cache_type, $server_name ) { - $default_conf_data['site_type'] = $site_type; - $default_conf_data['server_name'] = $server_name; - $default_conf_data['include_php_conf'] = $cache_type !== 'wpredis'; - $default_conf_data['include_wpsubdir_conf'] = $site_type === 'wpsubdir'; - $default_conf_data['include_redis_conf'] = $cache_type === 'wpredis'; - - return \EE\Utils\mustache_render( SITE_TEMPLATE_ROOT . '/config/nginx/default.conf.mustache', $default_conf_data ); - } - - /** - * Creates site root directory if does not exist. - * Throws error if it does exist. - */ - private function create_site_root() { - - if ( is_dir( $this->site_root ) ) { - EE::error( "Webroot directory for site $this->site_name already exists." ); - } + $default_conf_content = $default_conf_content = EE\Utils\mustache_render( SITE_TEMPLATE_ROOT . '/config/nginx/default.conf.mustache', [ 'server_name' => $this->site['name'] ] ); - if ( ! \EE\Utils\default_launch( "mkdir $this->site_root" ) ) { - EE::error( "Cannot create directory $this->site_root. Please check that folder permission allows easyengine to create directory there." ); - } + $env_data = [ + 'virtual_host' => $this->site['name'], + 'user_id' => $process_user['uid'], + 'group_id' => $process_user['gid'], + ]; + $env_content = EE\Utils\mustache_render( SITE_TEMPLATE_ROOT . '/config/.env.mustache', $env_data ); try { - $this->level = 1; - $whoami = EE::launch( "whoami", false, true ); + $this->fs->dumpFile( $site_docker_yml, $docker_compose_content ); + $this->fs->dumpFile( $site_conf_env, $env_content ); + $this->fs->mkdir( $site_conf_dir ); + $this->fs->mkdir( $site_conf_dir . '/nginx' ); + $this->fs->dumpFile( $site_nginx_default_conf, $default_conf_content ); + + $index_data = [ + 'version' => 'v' . EE_VERSION, + 'site_src_root' => $this->site['root'] . '/app/src', + ]; + $index_html = EE\Utils\mustache_render( SITE_TEMPLATE_ROOT . '/index.html.mustache', $index_data ); + $this->fs->mkdir( $site_src_dir ); + $this->fs->dumpFile( $site_src_dir . '/index.html', $index_html ); + + EE\Siteutils\add_site_redirects( $this->site['name'], $this->le ); - $terminal_username = rtrim( $whoami->stdout ); - - if ( ! chown( $this->site_root, $terminal_username ) ) { - throw new Exception( 'Could not change ownership of the site root. Please make sure you have appropriate rights.' ); - } - } - catch ( Exception $e ) { + EE::success( 'Configuration files copied.' ); + } catch ( Exception $e ) { $this->catch_clean( $e ); } - - return true; } - private function maybe_verify_remote_db_connection() { - if( 'db' === $this->db_host ) { - return; - } - - // Docker needs special handling if we want to connect to host machine. - // The since we're inside the container and we want to access host machine, - // we would need to replace localhost with default gateway - if( $this->db_host === '127.0.0.1' || $this->db_host === 'localhost' ) { - $launch = EE::launch( "docker network inspect $this->site_name --format='{{ (index .IPAM.Config 0).Gateway }}'", false, true ); - \EE\Utils\default_debug( $launch ); - - if( ! $launch->return_code ) { - $this->db_host = trim( $launch->stdout, "\n" ); - } - else { - throw new Exception( 'There was a problem inspecting network. Please check the logs' ); - } - } - \EE::log( 'Verifying connection to remote database' ); - - if( ! \EE\Utils\default_launch( "docker run -it --rm --network='$this->site_name' mysql sh -c \"mysql --host='$this->db_host' --port='$this->db_port' --user='$this->db_user' --password='$this->db_pass' --execute='EXIT'\"" ) ) { - throw new Exception( 'Unable to connect to remote db' ); - } - - \EE::success( 'Connection to remote db verified' ); - } - /** * Function to create the site. */ - private function create_site( $assoc_args ) { - $this->setup_site_network(); - try { - $this->maybe_verify_remote_db_connection(); - $this->configure_site(); - $this->level = 3; - EE::log( 'Pulling latest images. This may take some time.' ); - chdir( $this->site_root ); - \EE\Utils\default_launch( 'docker-compose pull' ); - EE::log( 'Starting site\'s services.' ); - if ( ! $this->docker::docker_compose_up( $this->site_root ) ) { - throw new Exception( 'There was some error in docker-compose up.' ); - } - } - catch ( Exception $e ) { - $this->catch_clean( $e ); - } - - $this->wp_download_and_config( $assoc_args ); - - if ( ! $this->skip_install ) { - $this->create_etc_hosts_entry(); - if ( ! $this->skip_chk ) { - $this->site_status_check(); - } - $this->install_wp(); - } - if ( $this->le ) { - $this->init_le(); - } - $this->info( array( $this->site_name ) ); - $this->create_site_db_entry(); - } - - /** - * Function to delete the given site. - * - * $this->level: - * Level of deletion. - * Level - 0: No need of clean-up. - * Level - 1: Clean-up only the site-root. - * Level - 2: Try to remove network. The network may or may not have been created. - * Level - 3: Disconnect & remove network and try to remove containers. The containers may not have been created. - * Level - 4: Remove containers. - * Level - 5: Remove db entry. - */ - private function delete_site() { - - // Commented below block intentionally as they need change in DB - // which should be discussed with the team - if ( 'db' !== $this->db_host && $this->level >= 4 ) { - - chdir( $this->site_root ); - $delete_db_command = "docker-compose exec php bash -c \"mysql --host=$this->db_host --port=$this->db_port --user=$this->db_user --password=$this->db_pass --execute='DROP DATABASE $this->db_name'\""; - - if ( \EE\Utils\default_launch( $delete_db_command ) ) { - EE::log( 'Database deleted.' ); - } else { - EE::warning( 'Could not remove the database.' ); - } - } - - if ( $this->level >= 3 ) { - if ( $this->docker::docker_compose_down( $this->site_root ) ) { - EE::log( "[$this->site_name] Docker Containers removed." ); - } else { - \EE\Utils\default_launch( "docker rm -f $(docker ps -q -f=label=created_by=EasyEngine -f=label=site_name=$this->site_name)" ); - if ( $this->level > 3 ) { - EE::warning( 'Error in removing docker containers.' ); - } - } - - $this->docker::disconnect_site_network_from( $this->site_name, $this->proxy_type ); - } - - if ( $this->level >= 2 ) { - if ( $this->docker::rm_network( $this->site_name ) ) { - EE::log( "[$this->site_name] Docker container removed from network $this->proxy_type." ); - } else { - if ( $this->level > 2 ) { - EE::warning( "Error in removing Docker container from network $this->proxy_type" ); - } - } - } - - if ( is_dir( $this->site_root ) ) { - if ( ! \EE\Utils\default_launch( "rm -rf $this->site_root" ) ) { - EE::error( 'Could not remove site root. Please check if you have sufficient rights.' ); - } - EE::log( "[$this->site_name] site root removed." ); - } - - if ( $this->level > 4 ) { - if ( $this->le ) { - EE::log( 'Removing ssl certs.' ); - $crt_file = EE_CONF_ROOT . "/nginx/certs/$this->site_name.crt"; - $key_file = EE_CONF_ROOT . "/nginx/certs/$this->site_name.key"; - $conf_certs = EE_CONF_ROOT . "/acme-conf/certs/$this->site_name"; - $conf_var = EE_CONF_ROOT . "/acme-conf/var/$this->site_name"; - // TODO: Change all these operations to use symfony filesystem - if ( file_exists( $conf_certs ) ) { - \EE\Utils\delete_dir( $conf_certs ); - } - if ( file_exists( $conf_var ) ) { - \EE\Utils\delete_dir( $conf_var ); - } - if ( file_exists( $crt_file ) ) { - unlink( $crt_file ); - } - if ( file_exists( $key_file ) ) { - unlink( $key_file ); - } - } - if ( $this->db::delete( array( 'sitename' => $this->site_name ) ) ) { - EE::log( 'Removing database entry.' ); - } else { - EE::error( 'Could not remove the database entry' ); - } - } - EE::log( "Site $this->site_name deleted." ); - } - - - /** - * Checking site is running or not [TESTING] - */ - private function site_status_check() { - $this->level = 4; - EE::log( 'Checking and verifying site-up status. This may take some time.' ); - $httpcode = '000'; - $ch = curl_init( $this->site_name ); - curl_setopt( $ch, CURLOPT_HEADER, true ); - curl_setopt( $ch, CURLOPT_NOBODY, true ); - curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); - curl_setopt( $ch, CURLOPT_TIMEOUT, 10 ); - - $i = 0; - try { - while ( 200 !== $httpcode && 302 !== $httpcode ) { - curl_exec( $ch ); - $httpcode = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); - echo '.'; - sleep( 2 ); - if ( $i ++ > 60 ) { - break; - } - } - if ( 200 !== $httpcode && 302 !== $httpcode ) { - throw new Exception( 'Problem connecting to site!' ); - } - } - catch ( Exception $e ) { - $this->catch_clean( $e ); - } - - } - - /** - * Function to setup site network. - */ - private function setup_site_network() { - - $this->level = 2; + private function create_site() { + $this->site['root'] = WEBROOT . $this->site['name']; + $this->level = 1; try { - if ( $this->docker::create_network( $this->site_name ) ) { - EE::success( 'Network started.' ); - } else { - throw new Exception( 'There was some error in starting the network.' ); - } + EE\Siteutils\create_site_root( $this->site['root'], $this->site['name'] ); + $this->level = 2; + EE\Siteutils\setup_site_network( $this->site['name'] ); $this->level = 3; + $this->configure_site_files(); - $this->docker::connect_site_network_to( $this->site_name, $this->proxy_type ); - } - catch ( Exception $e ) { - $this->catch_clean( $e ); - } - } - - private function wp_download_and_config( $assoc_args ) { - $core_download_args = array( - 'version', - 'skip-content', - ); - - $config_args = array( - 'dbprefix', - 'dbcharset', - 'dbcollate', - 'skip-check', - ); - - $core_download_arguments = ''; - if ( ! empty( $assoc_args ) ) { - foreach ( $assoc_args as $key => $value ) { - if ( in_array( $key, $core_download_args, true ) ) { - $core_download_arguments .= ' --' . $key . '=' . $value; - } - } - } - - $config_arguments = ''; - if ( ! empty( $assoc_args ) ) { - foreach ( $assoc_args as $key => $value ) { - if ( in_array( $key, $config_args, true ) ) { - $config_arguments .= ' --' . $key . '=' . $value; - } - } - } - - EE::log( 'Downloading and configuring WordPress.' ); - - $chown_command = "docker-compose exec --user=root php chown -R www-data: /var/www/"; - \EE\Utils\default_launch( $chown_command ); + EE\Siteutils\start_site_containers( $this->site['root'] ); - $core_download_command = "docker-compose exec --user='www-data' php wp core download --locale='" . $this->locale . "' " . $core_download_arguments; - \EE\Utils\default_launch( $core_download_command ); - - // TODO: Look for better way to handle mysql healthcheck - if ( 'db' === $this->db_host ) { - $mysql_unhealthy = true; - $health_chk = "docker-compose exec --user='www-data' php mysql --user='root' --password='$this->db_root_pass' --host='db' -e exit"; - $count = 0; - while ( $mysql_unhealthy ) { - $mysql_unhealthy = ! \EE\Utils\default_launch( $health_chk ); - if ( $count ++ > 30 ) { - break; - } - sleep( 1 ); - } - } - - $db_host = is_null( $this->db_port ) ? $this->db_host : "$this->db_host:$this->db_port"; - $wp_config_create_command = "docker-compose exec --user='www-data' php wp config create --dbuser='$this->db_user' --dbname='$this->db_name' --dbpass='$this->db_pass' --dbhost='$db_host' $config_arguments " . '--extra-php="if ( isset( \$_SERVER[\'HTTP_X_FORWARDED_PROTO\'] ) && \$_SERVER[\'HTTP_X_FORWARDED_PROTO\'] == \'https\'){\$_SERVER[\'HTTPS\']=\'on\';}"'; - - try { - if ( ! \EE\Utils\default_launch( $wp_config_create_command ) ) { - throw new Exception( "Couldn't connect to $this->db_host:$this->db_port or there was issue in `wp config create`. Please check logs." ); - } - if ( 'db' !== $this->db_host ) { - $name = str_replace( '_', '\_', $this->db_name ); - $check_db_exists = "docker-compose exec php bash -c \"mysqlshow --user='$this->db_user' --password='$this->db_pass' --host='$this->db_host' --port='$this->db_port' '$name'"; - - if ( ! \EE\Utils\default_launch( $check_db_exists ) ) { - EE::log( "Database `$this->db_name` does not exist. Attempting to create it." ); - $create_db_command = "docker-compose exec php bash -c \"mysql --host=$this->db_host --port=$this->db_port --user=$this->db_user --password=$this->db_pass --execute='CREATE DATABASE $this->db_name;'\""; - - if ( ! \EE\Utils\default_launch( $create_db_command ) ) { - throw new Exception( "Could not create database `$this->db_name` on `$this->db_host:$this->db_port`. Please check if $this->db_user has rights to create database or manually create a database and pass with `--dbname` parameter." ); - } - $this->level = 4; - } else { - if ( $this->force ) { - \EE\Utils\default_launch( "docker-compose exec --user='www-data' php wp db reset --yes" ); - } - $check_tables = "docker-compose exec --user='www-data' php wp db tables"; - if ( \EE\Utils\default_launch( $check_tables ) ) { - throw new Exception( "WordPress tables already seem to exist. Please backup and reset the database or use `--force` in the site create command to reset it." ); - } - } + EE\Siteutils\create_etc_hosts_entry( $this->site['name'] ); + if ( ! $this->skip_chk ) { + $this->level = 4; + EE\Siteutils\site_status_check( $this->site['name'] ); } - } - catch ( Exception $e ) { + } catch ( Exception $e ) { $this->catch_clean( $e ); } - } - - /** - * Function to create entry in /etc/hosts. - */ - private function create_etc_hosts_entry() { - - $host_line = LOCALHOST_IP . "\t$this->site_name"; - $etc_hosts = file_get_contents( '/etc/hosts' ); - if ( ! preg_match( "/\s+$this->site_name\$/m", $etc_hosts ) ) { - if ( \EE\Utils\default_launch( "/bin/bash -c 'echo \"$host_line\" >> /etc/hosts'" ) ) { - EE::success( 'Host entry successfully added.' ); - } else { - EE::warning( "Failed to add $this->site_name in host entry, Please do it manually!" ); - } - } else { - EE::log( 'Host entry already exists.' ); - } - } - - - /** - * Install wordpress with given credentials. - */ - private function install_wp() { - EE::log( "\nInstalling WordPress site." ); - chdir( $this->site_root ); - - $wp_install_command = 'install'; - $maybe_multisite_type = ''; - - if ( 'wpsubdom' === $this->site_type || 'wpsubdir' === $this->site_type ) { - $wp_install_command = 'multisite-install'; - $maybe_multisite_type = $this->site_type === 'wpsubdom' ? '--subdomains' : ''; - } - - $install_command = "docker-compose exec --user='www-data' php wp core $wp_install_command --url='$this->site_name' --title='$this->site_title' --admin_user='$this->site_user'" . ( $this->site_pass ? " --admin_password='$this->site_pass'" : '' ) . " --admin_email='$this->site_email' $maybe_multisite_type"; - - $core_install = \EE\Utils\default_launch( $install_command ); - - if ( ! $core_install ) { - EE::warning( 'WordPress install failed. Please check logs.' ); + if ( $this->le ) { + $this->init_le( $this->site['name'], $this->site['root'], false ); } - - $prefix = ( $this->le ) ? 'https://' : 'http://'; - EE::success( $prefix . $this->site_name . " has been created successfully!" ); + $this->info( [ $this->site['name'] ], [] ); + $this->create_site_db_entry(); } /** * Function to save the site configuration entry into database. */ private function create_site_db_entry() { - $ssl = $this->le ? 1 : 0; - $data = array( - 'sitename' => $this->site_name, - 'site_type' => $this->site_type, - 'site_title' => $this->site_title, - 'proxy_type' => $this->proxy_type, - 'cache_type' => $this->cache_type, - 'site_path' => $this->site_root, - 'db_name' => $this->db_name, - 'db_user' => $this->db_user, - 'db_host' => $this->db_host, - 'db_port' => $this->db_port, - 'db_password' => $this->db_pass, - 'db_root_password' => $this->db_root_pass, - 'email' => $this->site_email, - 'is_ssl' => $ssl, - 'created_on' => date( 'Y-m-d H:i:s', time() ), - ); - if ( ! $this->skip_install ) { - $data['wp_user'] = $this->site_user; - $data['wp_pass'] = $this->site_pass; - } + $ssl = $this->le ? 1 : 0; + $data = [ + 'sitename' => $this->site['name'], + 'site_type' => $this->site['type'], + 'site_path' => $this->site['root'], + 'site_command' => $this->command, + 'is_ssl' => $ssl, + 'created_on' => date( 'Y-m-d H:i:s', time() ), + ]; try { if ( $this->db::insert( $data ) ) { @@ -1207,8 +254,7 @@ private function create_site_db_entry() { } else { throw new Exception( 'Error creating site entry in database.' ); } - } - catch ( Exception $e ) { + } catch ( Exception $e ) { $this->catch_clean( $e ); } } @@ -1218,46 +264,49 @@ private function create_site_db_entry() { */ private function populate_site_info( $args ) { - $this->site_name = \EE\Utils\remove_trailing_slash( $args[0] ); - - if ( $this->db::site_in_db( $this->site_name ) ) { + $this->site['name'] = EE\Utils\remove_trailing_slash( $args[0] ); - $data = array( 'site_type', 'site_title', 'proxy_type', 'cache_type', 'site_path', 'db_name', 'db_user', 'db_host', 'db_port', 'db_password', 'db_root_password', 'wp_user', 'wp_pass', 'email', 'is_ssl' ); + if ( $this->db::site_in_db( $this->site['name'] ) ) { - $db_select = $this->db::select( $data, array( 'sitename' => $this->site_name ) ); + $db_select = $this->db::select( [], [ 'sitename' => $this->site['name'] ], 'sites', 1 ); - $this->site_type = $db_select[0]['site_type']; - $this->site_title = $db_select[0]['site_title']; - $this->proxy_type = $db_select[0]['proxy_type']; - $this->cache_type = $db_select[0]['cache_type']; - $this->site_root = $db_select[0]['site_path']; - $this->db_user = $db_select[0]['db_user']; - $this->db_name = $db_select[0]['db_name']; - $this->db_host = $db_select[0]['db_host']; - $this->db_port = $db_select[0]['db_port']; - $this->db_pass = $db_select[0]['db_password']; - $this->db_root_pass = $db_select[0]['db_root_password']; - $this->site_user = $db_select[0]['wp_user']; - $this->site_pass = $db_select[0]['wp_pass']; - $this->site_email = $db_select[0]['email']; - $this->le = $db_select[0]['is_ssl']; + $this->site['type'] = $db_select['site_type']; + $this->site['root'] = $db_select['site_path']; + $this->le = $db_select['is_ssl']; } else { - EE::error( "Site $this->site_name does not exist." ); + EE::error( sprintf( 'Site %s does not exist.', $this->site['name'] ) ); } } + /** + * @inheritdoc + */ + public function restart( $args, $assoc_args, $whitelisted_containers = [] ) { + $whitelisted_containers = [ 'nginx' ]; + parent::restart( $args, $assoc_args, $whitelisted_containers ); + } + + /** + * @inheritdoc + */ + public function reload( $args, $assoc_args, $whitelisted_containers = [], $reload_commands = [] ) { + $whitelisted_containers = [ 'nginx' ]; + parent::reload( $args, $assoc_args, $whitelisted_containers, $reload_commands = [] ); + } + /** * Catch and clean exceptions. * * @param Exception $e */ private function catch_clean( $e ) { - \EE\Utils\delem_log( 'site cleanup start' ); + + EE\Utils\delem_log( 'site cleanup start' ); EE::warning( $e->getMessage() ); EE::warning( 'Initiating clean-up.' ); - $this->delete_site(); - \EE\Utils\delem_log( 'site cleanup end' ); + $this->delete_site( $this->level, $this->site['name'], $this->site['root'] ); + EE\Utils\delem_log( 'site cleanup end' ); exit; } @@ -1265,9 +314,10 @@ private function catch_clean( $e ) { * Roll back on interrupt. */ private function rollback() { + EE::warning( 'Exiting gracefully after rolling back. This may take some time.' ); if ( $this->level > 0 ) { - $this->delete_site(); + $this->delete_site( $this->level, $this->site['name'], $this->site['root'] ); } EE::success( 'Rollback complete. Exiting now.' ); exit; @@ -1277,16 +327,15 @@ private function rollback() { * Shutdown function to catch and rollback from fatal errors. */ private function shutDownFunction() { + $error = error_get_last(); - if ( isset( $error ) ) { - if ( $error['type'] === E_ERROR ) { - EE::warning( 'An Error occurred. Initiating clean-up.' ); - $this->logger->error( 'Type: ' . $error['type'] ); - $this->logger->error( 'Message: ' . $error['message'] ); - $this->logger->error( 'File: ' . $error['file'] ); - $this->logger->error( 'Line: ' . $error['line'] ); - $this->rollback(); - } + if ( isset( $error ) && $error['type'] === E_ERROR ) { + EE::warning( 'An Error occurred. Initiating clean-up.' ); + $this->logger->error( 'Type: ' . $error['type'] ); + $this->logger->error( 'Message: ' . $error['message'] ); + $this->logger->error( 'File: ' . $error['file'] ); + $this->logger->error( 'Line: ' . $error['line'] ); + $this->rollback(); } } } diff --git a/src/Site_Docker.php b/src/Site_Docker.php index 8f25370d..d025914c 100644 --- a/src/Site_Docker.php +++ b/src/Site_Docker.php @@ -7,180 +7,51 @@ class Site_Docker { /** * Generate docker-compose.yml according to requirement. * - * @param array $filters Array of flags to determine the docker-compose.yml generation. - * Empty/Default -> Generates default WordPress docker-compose.yml - * ['le'] -> Enables letsencrypt in the generation. + * @param array $filters Array to determine the docker-compose.yml generation. * * @return String docker-compose.yml content string. */ public function generate_docker_compose_yml( array $filters = [] ) { - $base = array(); + $base = []; - $restart_default = array( 'name' => 'always' ); - $network_default = array( 'name' => 'site-network' ); - - // db configuration. - $db['service_name'] = array( 'name' => 'db' ); - $db['image'] = array( 'name' => 'easyengine/mariadb:v' . EE_VERSION ); - $db['restart'] = $restart_default; - $db['labels'] = array( - array( - 'label' => array( - 'name' => 'io.easyengine.site=${VIRTUAL_HOST}', - ), - ), - ); - $db['volumes'] = array( - array( - 'vol' => array( - 'name' => './app/db:/var/lib/mysql', - ), - ), - ); - $db['environment'] = array( - 'env' => array( - array( 'name' => 'MYSQL_ROOT_PASSWORD' ), - array( 'name' => 'MYSQL_DATABASE' ), - array( 'name' => 'MYSQL_USER' ), - array( 'name' => 'MYSQL_PASSWORD' ), - ), - ); - $db['networks'] = $network_default; - // PHP configuration. - $php['service_name'] = array( 'name' => 'php' ); - $php['image'] = array( 'name' => 'easyengine/php:v' . EE_VERSION ); - $php['depends_on'] = array( 'name' => 'db' ); - $php['restart'] = $restart_default; - $php['labels'] = array( - array( - 'label' => array( - 'name' => 'io.easyengine.site=${VIRTUAL_HOST}', - ), - ), - ); - $php['volumes'] = array( - array( - 'vol' => array( - array( 'name' => './app/src:/var/www/htdocs' ), - array( 'name' => './config/php-fpm/php.ini:/usr/local/etc/php/php.ini' ), - ), - ), - ); - $php['environment'] = array( - 'env' => array( - array( 'name' => 'WORDPRESS_DB_HOST' ), - array( 'name' => 'WORDPRESS_DB_NAME' ), - array( 'name' => 'WORDPRESS_DB_USER' ), - array( 'name' => 'WORDPRESS_DB_PASSWORD' ), - array( 'name' => 'USER_ID' ), - array( 'name' => 'GROUP_ID' ), - array( 'name' => 'VIRTUAL_HOST' ), - ), - ); - $php['networks'] = $network_default; + $restart_default = [ 'name' => 'always' ]; + $network_default = [ 'name' => 'site-network' ]; // nginx configuration. - $nginx['service_name'] = array( 'name' => 'nginx' ); - $nginx['image'] = array( 'name' => 'easyengine/nginx:v' . EE_VERSION ); - $nginx['depends_on'] = array( 'name' => 'php' ); + $nginx['service_name'] = [ 'name' => 'nginx' ]; + $nginx['image'] = [ 'name' => 'easyengine/nginx:v' . EE_VERSION ]; $nginx['restart'] = $restart_default; - $v_host = in_array( 'wpsubdom', $filters, true ) ? 'VIRTUAL_HOST=${VIRTUAL_HOST},*.${VIRTUAL_HOST}' : 'VIRTUAL_HOST'; - - $nginx['environment'] = array( - 'env' => array( - array( 'name' => $v_host ), - array( 'name' => 'VIRTUAL_PATH=/' ), - array( 'name' => 'HSTS=off' ), - ), - ); - $nginx['volumes'] = array( - 'vol' => array( - array( 'name' => './app/src:/var/www/htdocs' ), - array( 'name' => './config/nginx/default.conf:/etc/nginx/conf.d/default.conf' ), - array( 'name' => './logs/nginx:/var/log/nginx' ), - array( 'name' => './config/nginx/common:/usr/local/openresty/nginx/conf/common' ), - ), - ); - $nginx['labels'] = array( - array( - 'label' => array( - 'name' => 'io.easyengine.site=${VIRTUAL_HOST}', - ), - ), - ); + $v_host = 'VIRTUAL_HOST'; + + $nginx['environment'] = [ + 'env' => [ + [ 'name' => $v_host ], + [ 'name' => 'VIRTUAL_PATH=/' ], + [ 'name' => 'HSTS=off' ], + ], + ]; + $nginx['volumes'] = [ + 'vol' => [ + [ 'name' => './app/src:/var/www/htdocs' ], + [ 'name' => './config/nginx/default.conf:/etc/nginx/conf.d/default.conf' ], + [ 'name' => './logs/nginx:/var/log/nginx' ], + [ 'name' => './config/nginx/common:/usr/local/openresty/nginx/conf/common' ], + ], + ]; + $nginx['labels'] = [ + 'label' => [ + 'name' => 'io.easyengine.site=${VIRTUAL_HOST}', + ], + ]; $nginx['networks'] = $network_default; - // PhpMyAdmin configuration. - $phpmyadmin['service_name'] = array( 'name' => 'phpmyadmin' ); - $phpmyadmin['image'] = array( 'name' => 'easyengine/phpmyadmin:v' . EE_VERSION ); - $phpmyadmin['restart'] = $restart_default; - $phpmyadmin['environment'] = array( - 'env' => array( - array( 'name' => 'PMA_ABSOLUTE_URI=http://${VIRTUAL_HOST}/ee-admin/pma/' ), - array( 'name' => $v_host ), - array( 'name' => 'VIRTUAL_PATH=/ee-admin/pma/' ), - ), - ); - $phpmyadmin['labels'] = array( - array( - 'label' => array( - 'name' => 'io.easyengine.site=${VIRTUAL_HOST}', - ), - ), - ); - $phpmyadmin['networks'] = $network_default; - - // mailhog configuration. - $mailhog['service_name'] = array( 'name' => 'mailhog' ); - $mailhog['image'] = array( 'name' => 'easyengine/mailhog:v' . EE_VERSION ); - $mailhog['restart'] = $restart_default; - $mailhog['command'] = array( 'name' => '["-invite-jim=false"]' ); - $mailhog['environment'] = array( - 'env' => array( - array( 'name' => $v_host ), - array( 'name' => 'VIRTUAL_PATH=/ee-admin/mailhog/' ), - array( 'name' => 'VIRTUAL_PORT=8025' ), - ), - ); - $mailhog['labels'] = array( - array( - 'label' => array( - 'name' => 'io.easyengine.site=${VIRTUAL_HOST}', - ), - ), - ); - $mailhog['networks'] = $network_default; - - // redis configuration. - $redis['service_name'] = array( 'name' => 'redis' ); - $redis['image'] = array( 'name' => 'easyengine/redis:v' . EE_VERSION ); - $redis['labels'] = array( - array( - 'label' => array( - 'name' => 'io.easyengine.site=${VIRTUAL_HOST}', - ), - ), - ); - $redis['networks'] = $network_default; - - if ( in_array( 'db', $filters, true ) ) { - $base[] = $db; - } - - $base[] = $php; $base[] = $nginx; - $base[] = $mailhog; - $base[] = $phpmyadmin; - - if ( in_array( 'wpredis', $filters, true ) ) { - $base[] = $redis; - } - $binding = array( + $binding = [ 'services' => $base, 'network' => true, - ); + ]; $docker_compose_yml = mustache_render( SITE_TEMPLATE_ROOT . '/docker-compose.mustache', $binding ); diff --git a/src/Site_Letsencrypt.php b/src/Site_Letsencrypt.php deleted file mode 100644 index 21dc8909..00000000 --- a/src/Site_Letsencrypt.php +++ /dev/null @@ -1,537 +0,0 @@ -<?php - -use AcmePhp\Cli\Repository\Repository; -use AcmePhp\Cli\Serializer\PemEncoder; -use AcmePhp\Cli\Serializer\PemNormalizer; -use AcmePhp\Core\AcmeClient; -use AcmePhp\Core\Challenge\ChainValidator; -use AcmePhp\Core\Challenge\WaitingValidator; -use AcmePhp\Core\Challenge\Http\SimpleHttpSolver; -use AcmePhp\Core\Challenge\Http\HttpValidator; -use AcmePhp\Core\Challenge\Dns\SimpleDnsSolver; -use AcmePhp\Core\Challenge\Dns\DnsValidator; -use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException; -use AcmePhp\Core\Http\SecureHttpClient; -use AcmePhp\Core\Http\Base64SafeEncoder; -use AcmePhp\Core\Http\ServerErrorHandler; -use AcmePhp\Ssl\Certificate; -use AcmePhp\Ssl\CertificateRequest; -use AcmePhp\Ssl\CertificateResponse; -use AcmePhp\Ssl\DistinguishedName; -use AcmePhp\Ssl\Parser\KeyParser; -use AcmePhp\Ssl\Parser\CertificateParser; -use AcmePhp\Ssl\Generator\KeyPairGenerator; -use AcmePhp\Ssl\Signer\CertificateRequestSigner; -use AcmePhp\Ssl\Signer\DataSigner; -use Symfony\Component\Console\Helper\Table; -use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Serializer\Encoder\JsonEncoder; -use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; -use Symfony\Component\Serializer\Serializer; -use League\Flysystem\Filesystem; -use League\Flysystem\Adapter\Local; -use League\Flysystem\Adapter\NullAdapter; -use GuzzleHttp\Client; - - -class Site_Letsencrypt { - - private $accountKeyPair; - private $httpClient; - private $base64SafeEncoder; - private $keyParser; - private $dataSigner; - private $serverErrorHandler; - private $serializer; - private $master; - private $backup; - private $client; - private $repository; - private $conf_dir; - - function __construct() { - $this->conf_dir = EE_CONF_ROOT . '/acme-conf'; - $this->setRepository(); - $this->setAcmeClient(); - } - - private function setAcmeClient() { - - if ( ! $this->repository->hasAccountKeyPair() ) { - EE::debug( 'No account key pair was found, generating one.' ); - EE::debug( 'Generating a key pair' ); - - $keygen = new KeyPairGenerator(); - $accountKeyPair = $keygen->generateKeyPair(); - EE::debug( 'Key pair generated, storing' ); - $this->repository->storeAccountKeyPair( $accountKeyPair ); - } else { - EE::debug( 'Loading account keypair' ); - $accountKeyPair = $this->repository->loadAccountKeyPair(); - } - - $this->accountKeyPair ?? $this->accountKeyPair = $accountKeyPair; - - $secureHttpClient = $this->getSecureHttpClient(); - $csrSigner = new CertificateRequestSigner(); - - $this->client = new AcmeClient( $secureHttpClient, 'https://acme-v02.api.letsencrypt.org/directory', $csrSigner ); - - } - - private function setRepository( $enable_backup = false ) { - $this->serializer ?? $this->serializer = new Serializer( - [ new PemNormalizer(), new GetSetMethodNormalizer() ], - [ new PemEncoder(), new JsonEncoder() ] - ); - $this->master ?? $this->master = new Filesystem( new Local( $this->conf_dir ) ); - $this->backup ?? $this->backup = new Filesystem( new NullAdapter() ); - - $this->repository = new Repository( $this->serializer, $this->master, $this->backup, $enable_backup ); - } - - private function getSecureHttpClient() { - $this->httpClient ?? $this->httpClient = new Client(); - $this->base64SafeEncoder ?? $this->base64SafeEncoder = new Base64SafeEncoder(); - $this->keyParser ?? $this->keyParser = new KeyParser(); - $this->dataSigner ?? $this->dataSigner = new DataSigner(); - $this->serverErrorHandler ?? $this->serverErrorHandler = new ServerErrorHandler(); - - return new SecureHttpClient( - $this->accountKeyPair, - $this->httpClient, - $this->base64SafeEncoder, - $this->keyParser, - $this->dataSigner, - $this->serverErrorHandler - ); - } - - - public function register( $email ) { - try { - $this->client->registerAccount( null, $email ); - } - catch ( Exception $e ) { - EE::warning( $e->getMessage() ); - EE::warning( 'It seems you\'re in local environment or there is some issue with network, please check logs. Skipping letsencrypt.' ); - - return false; - } - EE::debug( "Account with email id: $email registered successfully!" ); - return true; - } - - public function authorize( Array $domains, $site_root, $wildcard = false ) { - $solver = $wildcard ? new SimpleDnsSolver( null, new ConsoleOutput() ) : new SimpleHttpSolver(); - $solverName = $wildcard ? 'dns-01' : 'http-01'; - try { - $order = $this->client->requestOrder( $domains ); - } - catch ( Exception $e ) { - EE::warning( $e->getMessage() ); - EE::warning( 'It seems you\'re in local environment or using non-public domain, please check logs. Skipping letsencrypt.' ); - - return false; - } - - $authorizationChallengesToSolve = []; - foreach ( $order->getAuthorizationsChallenges() as $domainKey => $authorizationChallenges ) { - $authorizationChallenge = null; - foreach ( $authorizationChallenges as $candidate ) { - if ( $solver->supports( $candidate ) ) { - $authorizationChallenge = $candidate; - EE::debug( 'Authorization challenge supported by solver. Solver: ' . $solverName . ' Challenge: ' . $candidate->getType() ); - break; - } - // Should not get here as we are handling it. - EE::debug( 'Authorization challenge not supported by solver. Solver: ' . $solverName . ' Challenge: ' . $candidate->getType() ); - } - if ( null === $authorizationChallenge ) { - throw new ChallengeNotSupportedException(); - } - EE::debug( 'Storing authorization challenge. Domain: ' . $domainKey . ' Challenge: ' . print_r( $authorizationChallenge->toArray(), true ) ); - - $this->repository->storeDomainAuthorizationChallenge( $domainKey, $authorizationChallenge ); - $authorizationChallengesToSolve[] = $authorizationChallenge; - } - - /** @var AuthorizationChallenge $authorizationChallenge */ - foreach ( $authorizationChallengesToSolve as $authorizationChallenge ) { - EE::debug( 'Solving authorization challenge: Domain: ' . $authorizationChallenge->getDomain() . ' Challenge: ' . print_r( $authorizationChallenge->toArray(), true ) ); - $solver->solve( $authorizationChallenge ); - } - - $this->repository->storeCertificateOrder( $domains, $order ); - - if ( ! $wildcard ) { - $token = $authorizationChallenge->toArray()['token']; - $payload = $authorizationChallenge->toArray()['payload']; - EE::launch( "mkdir -p $site_root/app/src/.well-known/acme-challenge/" ); - EE::debug( "Creating challange file $site_root/app/src/.well-known/acme-challenge/$token" ); - file_put_contents( "$site_root/app/src/.well-known/acme-challenge/$token", $payload ); - EE::launch( "chown www-data: $site_root/app/src/.well-known/acme-challenge/$token" ); - } - return true; - } - - public function check( Array $domains, $wildcard = false ) { - EE::debug( 'Starting check with solver ' . $wildcard ? 'dns' : 'http' ); - $solver = $wildcard ? new SimpleDnsSolver( null, new ConsoleOutput() ) : new SimpleHttpSolver(); - $validator = new ChainValidator( - [ - new WaitingValidator( new HttpValidator() ), - new WaitingValidator( new DnsValidator() ) - ] - ); - - $order = null; - if ( $this->repository->hasCertificateOrder( $domains ) ) { - $order = $this->repository->loadCertificateOrder( $domains ); - EE::debug( sprintf( 'Loading the authorization token for domains %s ...', implode( ', ', $domains ) ) ); - } - - $authorizationChallengeToCleanup = []; - foreach ( $domains as $domain ) { - if ( $order ) { - $authorizationChallenge = null; - $authorizationChallenges = $order->getAuthorizationChallenges( $domain ); - foreach ( $authorizationChallenges as $challenge ) { - if ( $solver->supports( $challenge ) ) { - $authorizationChallenge = $challenge; - break; - } - } - if ( null === $authorizationChallenge ) { - throw new ChallengeNotSupportedException(); - } - } else { - if ( ! $this->repository->hasDomainAuthorizationChallenge( $domain ) ) { - EE::error( "Domain: $domain not yet authorized/has not been started of with EasyEngine letsencrypt site creation." ); - } - $authorizationChallenge = $this->repository->loadDomainAuthorizationChallenge( $domain ); - if ( ! $solver->supports( $authorizationChallenge ) ) { - throw new ChallengeNotSupportedException(); - } - } - EE::debug( 'Challenge loaded.' ); - - $authorizationChallenge = $this->client->reloadAuthorization( $authorizationChallenge ); - if ( ! $authorizationChallenge->isValid() ) { - EE::debug( sprintf( 'Testing the challenge for domain %s', $domain ) ); - if ( ! $validator->isValid( $authorizationChallenge ) ) { - EE::warning( sprintf( 'Can not valid challenge for domain %s', $domain ) ); - } - - EE::debug( sprintf( 'Requesting authorization check for domain %s', $domain ) ); - try { - $this->client->challengeAuthorization( $authorizationChallenge ); - } - catch ( Exception $e ) { - EE::debug( $e->getMessage() ); - EE::warning( 'Challange Authorization failed. Check logs and check if your domain is pointed correctly to this server.' ); - $site_name = isset( $domains[1] ) ? $domains[1] : $domains[0]; - EE::log( "Re-run `ee site le $site_name` after fixing the issue." ); - - return false; - } - $authorizationChallengeToCleanup[] = $authorizationChallenge; - } - } - - EE::log( 'The authorization check was successful!' ); - - if ( $solver instanceof MultipleChallengesSolverInterface ) { - $solver->cleanupAll( $authorizationChallengeToCleanup ); - } else { - /** @var AuthorizationChallenge $authorizationChallenge */ - foreach ( $authorizationChallengeToCleanup as $authorizationChallenge ) { - $solver->cleanup( $authorizationChallenge ); - } - } - return true; - } - - public function request( $domain, $altNames = [], $email, $force=false ) { - $alternativeNames = array_unique( $altNames ); - sort( $alternativeNames ); - - // Certificate renewal - if ( $this->hasValidCertificate( $domain, $alternativeNames ) ) { - EE::debug( "Certificate found for $domain, executing renewal" ); - - return $this->executeRenewal( $domain, $alternativeNames, $force ); - } - - EE::debug( "No certificate found, executing first request for $domain" ); - - // Certificate first request - return $this->executeFirstRequest( $domain, $alternativeNames, $email ); - } - - /** - * Request a first certificate for the given domain. - * - * @param string $domain - * @param array $alternativeNames - */ - private function executeFirstRequest( $domain, array $alternativeNames, $email ) { - EE::log( 'Executing first request.' ); - - // Generate domain key pair - $keygen = new KeyPairGenerator(); - $domainKeyPair = $keygen->generateKeyPair(); - $this->repository->storeDomainKeyPair( $domain, $domainKeyPair ); - - EE::debug( "$domain Domain key pair generated and stored" ); - - $distinguishedName = $this->getOrCreateDistinguishedName( $domain, $alternativeNames, $email ); - // TODO: ask them ;) - EE::debug( 'Distinguished name informations have been stored locally for this domain (they won\'t be asked on renewal).' ); - - // Order - $domains = array_merge( [ $domain ], $alternativeNames ); - EE::debug( sprintf( 'Loading the order related to the domains %s .', implode( ', ', $domains ) ) ); - if ( ! $this->repository->hasCertificateOrder( $domains ) ) { - EE::error( "$domain has not yet been authorized." ); - } - $order = $this->repository->loadCertificateOrder( $domains ); - - // Request - EE::log( sprintf( 'Requesting first certificate for domain %s.', $domain ) ); - $csr = new CertificateRequest( $distinguishedName, $domainKeyPair ); - $response = $this->client->finalizeOrder( $order, $csr ); - EE::log( 'Certificate received' ); - - // Store - $this->repository->storeDomainCertificate( $domain, $response->getCertificate() ); - EE::log( 'Certificate stored' ); - - // Post-generate actions - $this->moveCertsToNginxProxy( $response ); - } - - private function moveCertsToNginxProxy( CertificateResponse $response ) { - $domain = $response->getCertificateRequest()->getDistinguishedName()->getCommonName(); - $privateKey = $response->getCertificateRequest()->getKeyPair()->getPrivateKey(); - $certificate = $response->getCertificate(); - - // To handle wildcard certs - $domain = ltrim( $domain, '*.' ); - - file_put_contents( EE_CONF_ROOT . '/nginx/certs/' . $domain . '.key', $privateKey->getPEM() ); - - // Issuer chain - $issuerChain = array_map( - function ( Certificate $certificate ) { - return $certificate->getPEM(); - }, $certificate->getIssuerChain() - ); - - // Full chain - $fullChainPem = $certificate->getPEM() . "\n" . implode( "\n", $issuerChain ); - - file_put_contents( EE_CONF_ROOT . '/nginx/certs/' . $domain . '.crt', $fullChainPem ); - } - - /** - * Renew a given domain certificate. - * - * @param string $domain - * @param array $alternativeNames - * @param bool $force - */ - private function executeRenewal( $domain, array $alternativeNames, $force = false ) { - try { - // Check expiration date to avoid too much renewal - EE::log( "Loading current certificate for $domain" ); - - $certificate = $this->repository->loadDomainCertificate( $domain ); - - if ( ! $force ) { - $certificateParser = new CertificateParser(); - $parsedCertificate = $certificateParser->parse( $certificate ); - - if ( $parsedCertificate->getValidTo()->format( 'U' ) - time() >= 604800 ) { - - EE::log( - sprintf( - 'Current certificate is valid until %s, renewal is not necessary.', - $parsedCertificate->getValidTo()->format( 'Y-m-d H:i:s' ) - ) - ); - - return; - } - - EE::log( - sprintf( - 'Current certificate will expire in less than a week (%s), renewal is required.', - $parsedCertificate->getValidTo()->format( 'Y-m-d H:i:s' ) - ) - ); - } else { - EE::log( 'Forced renewal.' ); - } - - // Key pair - EE::debug( 'Loading domain key pair...' ); - $domainKeyPair = $this->repository->loadDomainKeyPair( $domain ); - - // Distinguished name - EE::debug( 'Loading domain distinguished name...' ); - $distinguishedName = $this->getOrCreateDistinguishedName( $domain, $alternativeNames ); - - // Order - $domains = array_merge( [ $domain ], $alternativeNames ); - EE::debug( sprintf( 'Loading the order related to the domains %s.', implode( ', ', $domains ) ) ); - if ( ! $this->repository->hasCertificateOrder( $domains ) ) { - EE::error( "$domain has not yet been authorized." ); - } - $order = $this->repository->loadCertificateOrder( $domains ); - - // Renewal - EE::log( sprintf( 'Renewing certificate for domain %s.', $domain ) ); - $csr = new CertificateRequest( $distinguishedName, $domainKeyPair ); - $response = $this->client->finalizeOrder( $order, $csr ); - EE::log( 'Certificate received' ); - - $this->repository->storeDomainCertificate( $domain, $response->getCertificate() ); - $this->log( 'Certificate stored' ); - - // Post-generate actions - $this->moveCertsToNginxProxy( $response ); - EE::log( 'Certificate renewed successfully!' ); - - } - catch ( \Exception $e ) { - EE::warning( 'A critical error occured during certificate renewal' ); - EE::debug( print_r( $e, true ) ); - - throw $e; - } - catch ( \Throwable $e ) { - EE::warning( 'A critical error occured during certificate renewal' ); - EE::debug( print_r( $e, true ) ); - - throw $e; - } - } - - private function hasValidCertificate( $domain, array $alternativeNames ) { - if ( ! $this->repository->hasDomainCertificate( $domain ) ) { - return false; - } - - if ( ! $this->repository->hasDomainKeyPair( $domain ) ) { - return false; - } - - if ( ! $this->repository->hasDomainDistinguishedName( $domain ) ) { - return false; - } - - if ( $this->repository->loadDomainDistinguishedName( $domain )->getSubjectAlternativeNames() !== $alternativeNames ) { - return false; - } - - return true; - } - - /** - * Retrieve the stored distinguishedName or create a new one if needed. - * - * @param string $domain - * @param array $alternativeNames - * - * @return DistinguishedName - */ - private function getOrCreateDistinguishedName( $domain, array $alternativeNames, $email ) { - if ( $this->repository->hasDomainDistinguishedName( $domain ) ) { - $original = $this->repository->loadDomainDistinguishedName( $domain ); - - $distinguishedName = new DistinguishedName( - $domain, - $original->getCountryName(), - $original->getStateOrProvinceName(), - $original->getLocalityName(), - $original->getOrganizationName(), - $original->getOrganizationalUnitName(), - $original->getEmailAddress(), - $alternativeNames - ); - } else { - // Ask DistinguishedName - $distinguishedName = new DistinguishedName( - $domain, - // TODO: Ask and fill these values properly - 'US', - 'CA', - 'Mountain View', - 'Let\'s Encrypt', - 'Let\'s Encrypt Authority X3', - $email, - $alternativeNames - ); - - } - - $this->repository->storeDomainDistinguishedName( $domain, $distinguishedName ); - - return $distinguishedName; - } - - - public function status() { - $this->master ?? $this->master = new Filesystem( new Local( $this->conf_dir ) ); - - $certificateParser = new CertificateParser(); - - $table = new Table( $output ); - $table->setHeaders( [ 'Domain', 'Issuer', 'Valid from', 'Valid to', 'Needs renewal?' ] ); - - $directories = $this->master->listContents( 'certs' ); - - foreach ( $directories as $directory ) { - if ( 'dir' !== $directory['type'] ) { - continue; - } - - $parsedCertificate = $certificateParser->parse( $this->repository->loadDomainCertificate( $directory['basename'] ) ); - if ( ! $input->getOption( 'all' ) && $parsedCertificate->isExpired() ) { - continue; - } - $domainString = $parsedCertificate->getSubject(); - - $alternativeNames = array_diff( $parsedCertificate->getSubjectAlternativeNames(), [ $parsedCertificate->getSubject() ] ); - if ( count( $alternativeNames ) ) { - sort( $alternativeNames ); - $last = array_pop( $alternativeNames ); - foreach ( $alternativeNames as $alternativeName ) { - $domainString .= "\n ├── " . $alternativeName; - } - $domainString .= "\n └── " . $last; - } - - $table->addRow( - [ - $domainString, - $parsedCertificate->getIssuer(), - $parsedCertificate->getValidFrom()->format( 'Y-m-d H:i:s' ), - $parsedCertificate->getValidTo()->format( 'Y-m-d H:i:s' ), - ( $parsedCertificate->getValidTo()->format( 'U' ) - time() < 604800 ) ? '<comment>Yes</comment>' : 'No', - ] - ); - } - - $table->render(); - } - - public function cleanup( $site_root ) { - $challange_dir = "$site_root/app/src/.well-known"; - if ( file_exists( "$site_root/app/src/.well-known" ) ) { - EE::debug( 'Cleaning up webroot files.' ); - EE\Utils\delete_dir( $challange_dir ); - } - } -} diff --git a/templates/config/.env.mustache b/templates/config/.env.mustache index 3e2c283b..6d6c2f57 100644 --- a/templates/config/.env.mustache +++ b/templates/config/.env.mustache @@ -1,13 +1,3 @@ -{{#local}} -MYSQL_ROOT_PASSWORD={{root_password}} -MYSQL_DATABASE={{database_name}} -MYSQL_USER={{database_user}} -MYSQL_PASSWORD={{user_password}} -{{/local}} -WORDPRESS_DB_HOST={{wp_db_host}} -WORDPRESS_DB_USER={{wp_db_user}} -WORDPRESS_DB_NAME={{wp_db_name}} -WORDPRESS_DB_PASSWORD={{wp_db_pass}} VIRTUAL_HOST={{virtual_host}} VIRTUAL_HOST_EMAIL=example@{{virtual_host}} USER_ID={{user_id}} diff --git a/templates/config/nginx/default.conf.mustache b/templates/config/nginx/default.conf.mustache index f19ec679..281bec9f 100644 --- a/templates/config/nginx/default.conf.mustache +++ b/templates/config/nginx/default.conf.mustache @@ -9,118 +9,7 @@ server { server_name {{server_name}}; - index index.php index.html index.htm; - - {{#include_redis_conf}} - # Redis NGINX CONFIGURATION - set $skip 0; - # POST requests and URL with a query string should always go to php - if ($request_method = POST) { - set $skip 1; - } - if ($query_string != "") { - set $skip 1; - } - # Don't cache URL containing the following segments - if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|wp-.*.php|index.php|/feed/|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") { - set $skip 1; - } - # Don't use the cache for logged in users or recent commenter or customer with items in cart - if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|woocommerce_items_in_cart") { - set $skip 1; - } - # Use cached or actual file if they exists, Otherwise pass request to WordPress - location / { - try_files $uri $uri/ /index.php?$args; - } - - location /redis-fetch { - internal ; - set $redis_key $args; - redis_pass redis:6379; - } - location /redis-store { - internal ; - set_unescape_uri $key $arg_key ; - redis2_query set $key $echo_request_body; - redis2_query expire $key 14400; - redis2_pass redis:6379; - } - - location ~ \.php$ { - # add_header Cache-Control "max-age=0, no-cache, no-store, must-revalidate"; - set $key "nginx-cache:$scheme$request_method$host$request_uri"; - try_files $uri =404; - - srcache_fetch_skip $skip; - srcache_store_skip $skip; - - srcache_response_cache_control off; - - set_escape_uri $escaped_key $key; - - srcache_fetch GET /redis-fetch $key; - srcache_store PUT /redis-store key=$escaped_key; - - more_set_headers 'X-SRCache-Fetch-Status $srcache_fetch_status'; - more_set_headers 'X-SRCache-Store-Status $srcache_store_status'; - - include fastcgi_params; - fastcgi_pass php:9000; - } - - {{/include_redis_conf}} - - {{#include_wpsubdir_conf}} - # WPSUBDIRECTORY NGINX CONFIGURATION - if (!-e $request_filename) { - - # Redirect wp-admin to wp-admin/ - rewrite /wp-admin$ $scheme://$host$uri/ permanent; - - # Redirect wp-* files/folders - rewrite ^(/[^/]+)?(/wp-.*) $2 last; - - # Redirect other php files - rewrite ^(/[^/]+)?(/.*\.php) $2 last; - } - {{/include_wpsubdir_conf}} - - {{#include_php_conf}} - # PHP NGINX CONFIGURATION - location / { - try_files $uri $uri/ /index.php?$args; - } - location ~ \.php$ { - try_files $uri =404; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - include fastcgi_params; - fastcgi_pass php:9000; - } - {{/include_php_conf}} - - {{! wpcommon.conf }} - # WordPress COMMON SETTINGS - # Limit access to avoid brute force attack - location = /wp-login.php { - limit_req zone=one burst=1 nodelay; - include fastcgi_params; - fastcgi_pass php:9000; - } - # Disable wp-config.txt - location = /wp-config.txt { - deny all; - access_log off; - log_not_found off; - } - # Disallow php in upload folder - location /wp-content/uploads/ { - location ~ \.php$ { - #Prevent Direct Access Of PHP Files From Web Browsers - deny all; - } - } - {{! /wpcommon.conf }} + index index.html index.htm; {{! locations.conf }} # NGINX CONFIGURATION FOR COMMON LOCATION @@ -131,13 +20,6 @@ server { expires max; } - location = /robots.txt { - # Some WordPress plugin gererate robots.txt file - # Refer #340 issue - try_files $uri $uri/ /index.php?$args; - access_log off; - log_not_found off; - } # Cache static files location ~* \.(ogg|ogv|svg|svgz|eot|otf|woff|woff2|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf|swf)$ { add_header "Access-Control-Allow-Origin" "*"; @@ -175,10 +57,6 @@ server { stub_status on; access_log off; } - location ~ ^/(status|ping) { - include fastcgi_params; - fastcgi_pass php:9000; - } location ~* \.(css|js)$ { expires 1d; add_header Cache-Control "public, must-revalidate"; diff --git a/templates/index.html.mustache b/templates/index.html.mustache new file mode 100644 index 00000000..5ec3f583 --- /dev/null +++ b/templates/index.html.mustache @@ -0,0 +1,3 @@ +<h1>Congratulations! Your html site with EasyEngine {{version}} is working perfectly.</h1> +<br/> +<h4>You can copy your site files to {{site_src_root}}.</h4>