From 63b96d902232bb47160016a11f2efd111bf4982c Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Tue, 18 Dec 2018 14:56:48 +0530 Subject: [PATCH 1/8] Adding cloudflare sdk to composer Signed-off-by: Riddhesh Sanghvi --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index cbe79ce7..a6860194 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "guzzlehttp/guzzle": "^6.0", "league/flysystem": "^1.0.19", "symfony/serializer": "^3.0", - "webmozart/assert": "^1.0" + "webmozart/assert": "^1.0", + "cloudflare/sdk": "1.1.1" } } From b71048200aea5decb31128964b83067ba540a52b Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Tue, 18 Dec 2018 15:02:18 +0530 Subject: [PATCH 2/8] Tag packages to stable working versions Signed-off-by: Riddhesh Sanghvi --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index a6860194..e3d9172c 100644 --- a/composer.json +++ b/composer.json @@ -39,10 +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" } } From 9da55b4d34da43d7f18f9ff81726b36e3298f269 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Tue, 18 Dec 2018 15:19:02 +0530 Subject: [PATCH 3/8] Add initial cloudflare dns solver Signed-off-by: Riddhesh Sanghvi --- src/helper/SimpleDnsCloudflareSolver.php | 132 +++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/helper/SimpleDnsCloudflareSolver.php diff --git a/src/helper/SimpleDnsCloudflareSolver.php b/src/helper/SimpleDnsCloudflareSolver.php new file mode 100644 index 00000000..ca29d3a9 --- /dev/null +++ b/src/helper/SimpleDnsCloudflareSolver.php @@ -0,0 +1,132 @@ +extractor = null === $extractor ? new DnsDataExtractor() : $extractor; + $this->output = null === $output ? new NullOutput() : $output; + } + + /** + * {@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 ); + + $key = new \Cloudflare\API\Auth\APIKey( get_config_value( 'le-mail' ), get_config_value( 'cloudflare-api-key' ) ); + $adapter = new \Cloudflare\API\Adapter\Guzzle( $key ); + $zones = new \Cloudflare\API\Endpoints\Zones( $adapter ); + + $zone_guess = ''; + $possible_zones = []; + $zone_list = []; + + foreach ( $zones->listZones()->result as $zone ) { + $zone_list[] = $zone->name; + } + + // Guessing zone name using the method in: https://github.com/certbot/certbot/blob/f90561012241171ed8e0dd9996c703c384357eba/certbot/plugins/dns_common.py#L319 + // https://github.com/certbot/certbot/blob/f90561012241171ed8e0dd9996c703c384357eba/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py#L131 + + $guesses = explode( '.', $authorizationChallenge->getDomain() ); + 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; + break; + } + } + + $manual = empty( $zone_guess ) ? true : false; + + if ( ! $manual ) { + $zoneID = $zones->getZoneID( $zone_guess ); + $dns = new \Cloudflare\API\Endpoints\DNS( $adapter ); + if ( $dns->addRecord( $zoneID, "TXT", $recordName, $recordValue, 0, false ) === true ) { + EE::log( "Created DNS record: $recordName with value $recordValue." . PHP_EOL ); + EE::log( 'Waiting for the changes to propogate.' ); + } 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 ); + + + $this->output->writeln( + sprintf( + <<<'EOF' +You can now cleanup your DNS by removing the domain _acme-challenge.%s. +EOF + , + $recordName + ) + ); + } +} From f6affcf12d53e994b076f2c9857c4cb697032de8 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Tue, 18 Dec 2018 15:19:24 +0530 Subject: [PATCH 4/8] Use cloudflare solver if api key is set Signed-off-by: Riddhesh Sanghvi --- src/helper/Site_Letsencrypt.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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; } From 262cde877257e22c742c75e80c6de7d5ab9abd40 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Tue, 18 Dec 2018 15:21:39 +0530 Subject: [PATCH 5/8] Update ssl code for clouflare integration Signed-off-by: Riddhesh Sanghvi --- src/helper/class-ee-site.php | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/helper/class-ee-site.php b/src/helper/class-ee-site.php index 16a76848..f2c24666 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,7 +812,8 @@ 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 { $this->ssl( [], [], $www_or_non_www ); @@ -828,7 +830,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 +908,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 +923,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 +950,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 +1026,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." ); } From b2b45163b9874df27d043f8365f8f3f9f9441270 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Tue, 18 Dec 2018 17:55:03 +0530 Subject: [PATCH 6/8] Add function to get zone and record-id Signed-off-by: Riddhesh Sanghvi --- src/helper/SimpleDnsCloudflareSolver.php | 117 ++++++++++++++++------- 1 file changed, 84 insertions(+), 33 deletions(-) diff --git a/src/helper/SimpleDnsCloudflareSolver.php b/src/helper/SimpleDnsCloudflareSolver.php index ca29d3a9..076099c6 100644 --- a/src/helper/SimpleDnsCloudflareSolver.php +++ b/src/helper/SimpleDnsCloudflareSolver.php @@ -23,6 +23,16 @@ class SimpleDnsCloudflareSolver implements SolverInterface { */ protected $output; + /** + * @var \Cloudflare\API\Endpoints\DNS + */ + protected $dns; + + /** + * @var \Cloudflare\API\Endpoints\Zones + */ + protected $zones; + /** * @param DnsDataExtractor $extractor * @param OutputInterface $output @@ -30,6 +40,12 @@ class SimpleDnsCloudflareSolver implements SolverInterface { public function __construct( DnsDataExtractor $extractor = null, OutputInterface $output = null ) { $this->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 ); + + } /** @@ -46,42 +62,14 @@ public function solve( AuthorizationChallenge $authorizationChallenge ) { $recordName = $this->extractor->getRecordName( $authorizationChallenge ); $recordValue = $this->extractor->getRecordValue( $authorizationChallenge ); - $key = new \Cloudflare\API\Auth\APIKey( get_config_value( 'le-mail' ), get_config_value( 'cloudflare-api-key' ) ); - $adapter = new \Cloudflare\API\Adapter\Guzzle( $key ); - $zones = new \Cloudflare\API\Endpoints\Zones( $adapter ); - - $zone_guess = ''; - $possible_zones = []; - $zone_list = []; - - foreach ( $zones->listZones()->result as $zone ) { - $zone_list[] = $zone->name; - } - - // Guessing zone name using the method in: https://github.com/certbot/certbot/blob/f90561012241171ed8e0dd9996c703c384357eba/certbot/plugins/dns_common.py#L319 - // https://github.com/certbot/certbot/blob/f90561012241171ed8e0dd9996c703c384357eba/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py#L131 - - $guesses = explode( '.', $authorizationChallenge->getDomain() ); - 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; - break; - } - } - - $manual = empty( $zone_guess ) ? true : false; + $zone_guess = $this->get_zone_name( $authorizationChallenge->getDomain() ); + $manual = empty( $zone_guess ) ? true : false; if ( ! $manual ) { - $zoneID = $zones->getZoneID( $zone_guess ); - $dns = new \Cloudflare\API\Endpoints\DNS( $adapter ); - if ( $dns->addRecord( $zoneID, "TXT", $recordName, $recordValue, 0, false ) === true ) { + $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 ); - EE::log( 'Waiting for the changes to propogate.' ); } else { $manual = true; } @@ -129,4 +117,67 @@ public function cleanup( AuthorizationChallenge $authorizationChallenge ) { ) ); } + + /** + * 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; + } } From af6c67ef464345a714eb9d534d0b5e4183fd0f97 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Tue, 18 Dec 2018 17:56:35 +0530 Subject: [PATCH 7/8] Add cleanup of dns entries using cloudflare integration Signed-off-by: Riddhesh Sanghvi --- src/helper/SimpleDnsCloudflareSolver.php | 27 ++++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/helper/SimpleDnsCloudflareSolver.php b/src/helper/SimpleDnsCloudflareSolver.php index 076099c6..f46fb9de 100644 --- a/src/helper/SimpleDnsCloudflareSolver.php +++ b/src/helper/SimpleDnsCloudflareSolver.php @@ -104,18 +104,27 @@ public function solve( AuthorizationChallenge $authorizationChallenge ) { * {@inheritdoc} */ public function cleanup( AuthorizationChallenge $authorizationChallenge ) { - $recordName = $this->extractor->getRecordName( $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' ); - $this->output->writeln( - sprintf( - <<<'EOF' -You can now cleanup your DNS by removing the domain _acme-challenge.%s. + 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 - ) - ); + , + $recordName + ) + ); + } + } } /** From 531c648473325e88cd0821adc2e1df6a7c3fff7f Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Tue, 18 Dec 2018 19:07:55 +0530 Subject: [PATCH 8/8] Add minimal sleep for dns propagation Signed-off-by: Riddhesh Sanghvi --- src/helper/class-ee-site.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/helper/class-ee-site.php b/src/helper/class-ee-site.php index f2c24666..efacfe2c 100644 --- a/src/helper/class-ee-site.php +++ b/src/helper/class-ee-site.php @@ -816,6 +816,10 @@ protected function init_le( $site_url, $site_fs_path, $wildcard = false, $www_or 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 ); } }