diff --git a/composer.json b/composer.json
index cbe79ce7..e3d9172c 100644
--- a/composer.json
+++ b/composer.json
@@ -39,9 +39,10 @@
"require": {
"acmephp/core": "1.0.0",
"ext-openssl": "*",
- "guzzlehttp/guzzle": "^6.0",
- "league/flysystem": "^1.0.19",
- "symfony/serializer": "^3.0",
- "webmozart/assert": "^1.0"
+ "guzzlehttp/guzzle": "6.3.3",
+ "league/flysystem": "1.0.49",
+ "symfony/serializer": "3.4.20",
+ "webmozart/assert": "1.3.0",
+ "cloudflare/sdk": "1.1.1"
}
}
diff --git a/src/helper/SimpleDnsCloudflareSolver.php b/src/helper/SimpleDnsCloudflareSolver.php
new file mode 100644
index 00000000..f46fb9de
--- /dev/null
+++ b/src/helper/SimpleDnsCloudflareSolver.php
@@ -0,0 +1,192 @@
+extractor = null === $extractor ? new DnsDataExtractor() : $extractor;
+ $this->output = null === $output ? new NullOutput() : $output;
+ $key = new \Cloudflare\API\Auth\APIKey( get_config_value( 'le-mail' ), get_config_value( 'cloudflare-api-key' ) );
+ $adapter = new \Cloudflare\API\Adapter\Guzzle( $key );
+ $this->dns = new \Cloudflare\API\Endpoints\DNS( $adapter );
+ $this->zones = new \Cloudflare\API\Endpoints\Zones( $adapter );
+
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports( AuthorizationChallenge $authorizationChallenge ) {
+ return 'dns-01' === $authorizationChallenge->getType();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function solve( AuthorizationChallenge $authorizationChallenge ) {
+ $recordName = $this->extractor->getRecordName( $authorizationChallenge );
+ $recordValue = $this->extractor->getRecordValue( $authorizationChallenge );
+
+ $zone_guess = $this->get_zone_name( $authorizationChallenge->getDomain() );
+ $manual = empty( $zone_guess ) ? true : false;
+
+ if ( ! $manual ) {
+ $zoneID = $this->zones->getZoneID( $zone_guess );
+
+ if ( $this->dns->addRecord( $zoneID, "TXT", $recordName, $recordValue, 0, false ) === true ) {
+ EE::log( "Created DNS record: $recordName with value $recordValue." . PHP_EOL );
+ } else {
+ $manual = true;
+ }
+ }
+
+ if ( $manual ) {
+
+ EE::log( "Couldn't add dns record using cloudlfare API. Re-check the config values of `le-mail` and `cloudflare-api-key`." );
+
+ $this->output->writeln(
+ sprintf(
+ <<<'EOF'
+ Add the following TXT record to your DNS zone
+ Domain: %s
+ TXT value: %s
+
+ Wait for the propagation before moving to the next step
+ Tips: Use the following command to check the propagation
+
+ host -t TXT %s
+EOF
+ ,
+ $recordName,
+ $recordValue,
+ $recordName
+ )
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function cleanup( AuthorizationChallenge $authorizationChallenge ) {
+
+ $recordName = $this->extractor->getRecordName( $authorizationChallenge );
+ $zone = $this->get_zone_name( $authorizationChallenge->getDomain() );
+ $zoneID = $this->zones->getZoneID( $zone );
+ $record_ids = $this->get_record_id( $authorizationChallenge->getDomain(), $recordName, 'TXT' );
+
+ foreach ( $record_ids as $record_id ) {
+ if ( $this->dns->deleteRecord( $zoneID, $record_id ) ) {
+ EE::log( "Cleaned up DNS record: _acme-challenge.$recordName" );
+ } else {
+ $this->output->writeln(
+ sprintf(
+ <<<'EOF'
+ You can now cleanup your DNS by removing the domain _acme-challenge.%s.
+EOF
+ ,
+ $recordName
+ )
+ );
+ }
+ }
+ }
+
+ /**
+ * Function to get zone name of clouflare account from the given domain name.
+ * Guessing zone name using the method in:
+ * https://github.com/certbot/certbot/blob/f90561012241171ed8e0dd9996c703c384357eba/certbot/plugins/dns_common.py#L31
+ * https://github.com/certbot/certbot/blob/f90561012241171ed8e0dd9996c703c384357eba/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py#L131
+ *
+ * @param string $domain domain name.
+ *
+ * @return string found zone name.
+ */
+ private function get_zone_name( $domain ) {
+
+ $zone_guess = '';
+ $possible_zones = [];
+ $zone_list = [];
+
+ foreach ( $this->zones->listZones()->result as $zone ) {
+ $zone_list[] = $zone->name;
+ }
+
+ $guesses = explode( '.', $domain );
+ do {
+ $possible_zones[] = implode( '.', $guesses );
+ array_shift( $guesses );
+ } while ( ! empty( $guesses ) );
+
+ foreach ( $possible_zones as $possible_zone ) {
+ if ( in_array( $possible_zone, $zone_list, true ) ) {
+ $zone_guess = $possible_zone;
+
+ return $zone_guess;
+ }
+ }
+
+ return $zone_guess;
+ }
+
+ /**
+ * Function to get record id of cloudflare for a give record name and type.
+ *
+ * @param string $domain
+ * @param $record_name
+ * @param $record_type
+ *
+ * @return array of found record ids.
+ * @throws \Cloudflare\API\Endpoints\EndpointException
+ */
+ private function get_record_id( $domain, $record_name, $record_type ) {
+
+ $zone = $this->get_zone_name( $domain );
+ $zoneID = $this->zones->getZoneID( $zone );
+ $record_id = [];
+ $record_name = rtrim( $record_name, '.' );
+
+ foreach ( $this->dns->listRecords( $zoneID )->result as $record ) {
+ if ( ( $record->name === $record_name ) && ( $record->type === $record_type ) ) {
+ $record_id[] = $record->id;
+ }
+ }
+
+ return $record_id;
+ }
+}
diff --git a/src/helper/Site_Letsencrypt.php b/src/helper/Site_Letsencrypt.php
index 90489c93..568b8904 100644
--- a/src/helper/Site_Letsencrypt.php
+++ b/src/helper/Site_Letsencrypt.php
@@ -9,6 +9,7 @@
use AcmePhp\Core\Challenge\ChainValidator;
use AcmePhp\Core\Challenge\Dns\DnsValidator;
use AcmePhp\Core\Challenge\Dns\SimpleDnsSolver;
+use AcmePhp\Core\Challenge\Dns\SimpleDnsCloudflareSolver;
use AcmePhp\Core\Challenge\Http\HttpValidator;
use AcmePhp\Core\Challenge\Http\SimpleHttpSolver;
use AcmePhp\Core\Challenge\WaitingValidator;
@@ -33,6 +34,7 @@
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;
use function EE\Site\Utils\reload_global_nginx_proxy;
+use function EE\Utils\get_config_value;
class Site_Letsencrypt {
@@ -139,8 +141,12 @@ public function register( $email ) {
*/
public function authorize( Array $domains, $wildcard = false, $preferred_challenge = '' ) {
$is_solver_dns = ( $wildcard || 'dns' === $preferred_challenge ) ? true : false;
- $solver = $is_solver_dns ? new SimpleDnsSolver( null, new ConsoleOutput() ) : new SimpleHttpSolver();
- $solverName = $is_solver_dns ? 'dns-01' : 'http-01';
+ if ( $is_solver_dns ) {
+ $solver = empty ( get_config_value( 'cloudflare-api-key' ) ) ? new SimpleDnsSolver( null, new ConsoleOutput() ) : new SimpleDnsCloudflareSolver( null, new ConsoleOutput() );
+ } else {
+ $solver = new SimpleHttpSolver();
+ }
+ $solverName = $is_solver_dns ? 'dns-01' : 'http-01';
try {
$order = $this->client->requestOrder( $domains );
} catch ( \Exception $e ) {
@@ -199,7 +205,11 @@ public function authorize( Array $domains, $wildcard = false, $preferred_challen
public function check( Array $domains, $wildcard = false, $preferred_challenge = '' ) {
$is_solver_dns = ( $wildcard || 'dns' === $preferred_challenge ) ? true : false;
\EE::debug( ( 'Starting check with solver ' ) . ( $is_solver_dns ? 'dns' : 'http' ) );
- $solver = $is_solver_dns ? new SimpleDnsSolver( null, new ConsoleOutput() ) : new SimpleHttpSolver();
+ if ( $is_solver_dns ) {
+ $solver = empty ( get_config_value( 'cloudflare-api-key' ) ) ? new SimpleDnsSolver( null, new ConsoleOutput() ) : new SimpleDnsCloudflareSolver( null, new ConsoleOutput() );
+ } else {
+ $solver = new SimpleHttpSolver();
+ }
$validator = new ChainValidator(
[
new WaitingValidator( new HttpValidator() ),
@@ -254,7 +264,7 @@ public function check( Array $domains, $wildcard = false, $preferred_challenge =
$site_name = isset( $domains[1] ) ? $domains[1] : $domains[0];
$site_name = str_replace( '*.', '', $site_name );
-
+
\EE::log( "Re-run `ee site ssl $site_name` after fixing the issue." );
throw $e;
}
diff --git a/src/helper/class-ee-site.php b/src/helper/class-ee-site.php
index 16a76848..efacfe2c 100644
--- a/src/helper/class-ee-site.php
+++ b/src/helper/class-ee-site.php
@@ -9,6 +9,7 @@
use function EE\Utils\download;
use function EE\Utils\extract_zip;
use function EE\Utils\get_flag_value;
+use function EE\Utils\get_config_value;
use function EE\Utils\delem_log;
use function EE\Site\Utils\auto_site_name;
use function EE\Site\Utils\get_site_info;
@@ -791,7 +792,7 @@ protected function init_ssl( $site_url, $site_fs_path, $ssl_type, $wildcard = fa
* @param bool $www_or_non_www Allow LetsEncrypt on www or non-www subdomain.
*/
protected function init_le( $site_url, $site_fs_path, $wildcard = false, $www_or_non_www ) {
- $preferred_challenge = \EE\Utils\get_config_value( 'preferred_ssl_challenge', '' );
+ $preferred_challenge = get_config_value( 'preferred_ssl_challenge', '' );
$is_solver_dns = ( $wildcard || 'dns' === $preferred_challenge ) ? true : false;
\EE::debug( 'Wildcard in init_le: ' . ( bool ) $wildcard );
@@ -811,9 +812,14 @@ protected function init_le( $site_url, $site_fs_path, $wildcard = false, $www_or
if ( ! $client->authorize( $domains, $wildcard, $preferred_challenge ) ) {
return;
}
- if ( $is_solver_dns ) {
+ $api_key_absent = empty( get_config_value( 'cloudflare-api-key' ) );
+ if ( $is_solver_dns && $api_key_absent ) {
echo \cli\Colors::colorize( '%YIMPORTANT:%n Run `ee site ssl ' . $site_url . '` once the DNS changes have propagated to complete the certification generation and installation.', null );
} else {
+ if ( ! $api_key_absent && $is_solver_dns ) {
+ EE::log( 'Waiting for DNS entry propagation.' );
+ sleep( 10 );
+ }
$this->ssl( [], [], $www_or_non_www );
}
}
@@ -828,7 +834,7 @@ protected function init_le( $site_url, $site_fs_path, $wildcard = false, $www_or
* @return array
*/
private function get_cert_domains( string $site_url, $wildcard, $www_or_non_www = false ): array {
- $preferred_challenge = \EE\Utils\get_config_value( 'preferred_ssl_challenge', '' );
+ $preferred_challenge = get_config_value( 'preferred_ssl_challenge', '' );
$is_solver_dns = ( $wildcard || 'dns' === $preferred_challenge ) ? true : false;
$domains = [ $site_url ];
@@ -906,7 +912,8 @@ public function ssl( $args = [], $assoc_args = [], $www_or_non_www = false ) {
EE::log( 'Starting SSL verification.' );
// This checks if this method was called internally by ee or by user
- $called_by_ee = isset( $this->site_data['site_url'] );
+ $called_by_ee = ! empty( $this->site_data['site_url'] );
+ $api_key_absent = empty( get_config_value( 'cloudflare-api-key' ) );
if ( ! $called_by_ee ) {
$this->site_data = get_site_info( $args );
@@ -920,15 +927,20 @@ public function ssl( $args = [], $assoc_args = [], $www_or_non_www = false ) {
$domains = $this->get_cert_domains( $this->site_data['site_url'], $this->site_data['site_ssl_wildcard'], $www_or_non_www );
$client = new Site_Letsencrypt();
- $preferred_challenge = \EE\Utils\get_config_value( 'preferred_ssl_challenge', '' );
+ $preferred_challenge = get_config_value( 'preferred_ssl_challenge', '' );
try {
$client->check( $domains, $this->site_data['site_ssl_wildcard'], $preferred_challenge );
} catch ( \Exception $e ) {
- if ( $called_by_ee ) {
+ if ( $called_by_ee && $api_key_absent ) {
throw $e;
}
- EE::error( 'Failed to verify SSL: ' . $e->getMessage() );
+ $is_solver_dns = ( $this->site_data['site_ssl_wildcard'] || 'dns' === $preferred_challenge ) ? true : false;
+ $api_key_present = ! empty( get_config_value( 'cloudflare-api-key' ) );
+
+ $warning = ( $is_solver_dns && $api_key_present ) ? "The dns entries have not yet propogated. Manually check: \nhost -t TXT _acme-challenge." . $this->site_data['site_url'] . "\nBefore retrying `ee site ssl " . $this->site_data['site_url'] . "`" : 'Failed to verify SSL: ' . $e->getMessage();
+ EE::warning( $warning );
+ EE::warning( sprintf( 'Check logs and retry `ee site ssl %s` once the issue is resolved.', $this->site_data['site_url'] ) );
return;
}
@@ -942,7 +954,7 @@ public function ssl( $args = [], $assoc_args = [], $www_or_non_www = false ) {
reload_global_nginx_proxy();
- EE::log( 'SSL verification completed.' );
+ EE::success( 'SSL verification completed.' );
}
/**
@@ -1018,7 +1030,7 @@ public function publish( $args, $assoc_args ) {
if ( ! empty( $token ) ) {
EE::exec( "$ngrok authtoken $token" );
}
- $config_80_port = \EE\Utils\get_config_value( 'proxy_80_port', 80 );
+ $config_80_port = get_config_value( 'proxy_80_port', 80 );
if ( ! $refresh ) {
EE::log( "Publishing site: {$this->site_data->site_url} online." );
}