Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
192 changes: 192 additions & 0 deletions src/helper/SimpleDnsCloudflareSolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<?php

namespace AcmePhp\Core\Challenge\Dns;

use EE;
use AcmePhp\Core\Challenge\SolverInterface;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use function EE\Utils\get_config_value;

/**
* ACME DNS solver with cloudflare integration.
*/
class SimpleDnsCloudflareSolver implements SolverInterface {
/**
* @var DnsDataExtractor
*/
private $extractor;

/**
* @var OutputInterface
*/
protected $output;

/**
* @var \Cloudflare\API\Endpoints\DNS
*/
protected $dns;

/**
* @var \Cloudflare\API\Endpoints\Zones
*/
protected $zones;

/**
* @param DnsDataExtractor $extractor
* @param OutputInterface $output
*/
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 );


}

/**
* {@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

<comment>Wait for the propagation before moving to the next step</comment>
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 <comment>_acme-challenge.%s.</comment>
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;
}
}
18 changes: 14 additions & 4 deletions src/helper/Site_Letsencrypt.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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() ),
Expand Down Expand Up @@ -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;
}
Expand Down
30 changes: 21 additions & 9 deletions src/helper/class-ee-site.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 );

Expand All @@ -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 );
}
}
Expand All @@ -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 ];
Expand Down Expand Up @@ -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 );
Expand All @@ -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;
}
Expand All @@ -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.' );
}

/**
Expand Down Expand Up @@ -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." );
}
Expand Down