Skip to content
Open
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
43 changes: 40 additions & 3 deletions src/helper/Site_Letsencrypt.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use AcmePhp\Core\Challenge\WaitingValidator;
use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException;
use AcmePhp\Core\Exception\Protocol\CertificateRevocationException;
use AcmePhp\Core\Exception\Server\RateLimitedServerException;
use AcmePhp\Core\Protocol\AuthorizationChallenge;
use AcmePhp\Core\Protocol\ResourcesDirectory;
use AcmePhp\Core\Protocol\RevocationReason;
Expand Down Expand Up @@ -208,7 +209,12 @@ public function authorize( Array $domains, $wildcard = false, $preferred_challen
try {
$order = $this->client->requestOrder( $domains );
} catch ( \Exception $e ) {
\EE::warning( 'It seems you\'re in local environment or using non-public domain, please check logs. Skipping letsencrypt.' );
// A rate-limit is a distinct failure from a non-public domain; emit a clear, actionable message for it.
if ( $this->is_rate_limit_exception( $e ) ) {
\EE::warning( 'Let\'s Encrypt rate limit hit for: ' . implode( ', ', $domains ) . '. Please wait before retrying. Ref: https://letsencrypt.org/docs/rate-limits/' );
} else {
\EE::warning( 'It seems you\'re in local environment or using non-public domain, please check logs. Skipping letsencrypt.' );
}
\EE::log( 'You can fix the issue and re-run: ee site ssl-verify ' . $domains[0] );

return false;
Expand Down Expand Up @@ -279,6 +285,11 @@ public function revokeAuthorizationChallenges( array $domains ) {
\EE::debug( 'Domain Authorization Challenge for ' . $domain . ' revoked successfully' );
} catch ( CertificateRevocationException | AcmeCliException $e ) {
\EE::debug( $e->getMessage() );
} catch ( RateLimitedServerException $e ) {
// Revoking uses new-order too; stop here and let authorize() report the rate limit.
\EE::debug( $e->getMessage() );

return;
}
} else {
\EE::debug( 'Domain Authorization Challenge for ' . $domain . ' not found locally' );
Expand Down Expand Up @@ -568,6 +579,22 @@ public function isRenewalNecessary( $domain ) {
return true;
}

/**
* Whether the given exception is a Let's Encrypt `rateLimited` ACME error.
*
* @param \Throwable $e
*
* @return bool
*/
private function is_rate_limit_exception( $e ) {
if ( $e instanceof RateLimitedServerException ) {
return true;
}

// No bare "too many" match: it also hits unrelated errors like "Too many open files".
return false !== stripos( $e->getMessage(), 'ratelimited' );
}

/**
* Renew a given domain certificate.
*
Expand Down Expand Up @@ -644,15 +671,25 @@ private function executeRenewal( $domain, array $alternativeNames, $force = fals
\EE::warning( 'A critical error occured during certificate renewal' );
\EE::debug( print_r( $e, true ) );

\EE::warning( 'Challenge Authorization failed. Check logs and check if your domain is pointed correctly to this server.' );
// A rate-limit is not a misconfigured-domain failure; point the user to the LE rate-limit docs instead.
if ( $this->is_rate_limit_exception( $e ) ) {
\EE::warning( 'Let\'s Encrypt rate limit hit for: ' . $domain . '. Please wait before retrying. Ref: https://letsencrypt.org/docs/rate-limits/' );
} else {
\EE::warning( 'Challenge Authorization failed. Check logs and check if your domain is pointed correctly to this server.' );
}
\EE::log( 'You can fix the issue and re-run: ee site ssl-verify ' . $domains[0] );

return false;
} catch ( \Throwable $e ) {
\EE::warning( 'A critical error occured during certificate renewal' );
\EE::debug( print_r( $e, true ) );

\EE::warning( 'Challenge Authorization failed. Check logs and check if your domain is pointed correctly to this server.' );
// A rate-limit is not a misconfigured-domain failure; point the user to the LE rate-limit docs instead.
if ( $this->is_rate_limit_exception( $e ) ) {
\EE::warning( 'Let\'s Encrypt rate limit hit for: ' . $domain . '. Please wait before retrying. Ref: https://letsencrypt.org/docs/rate-limits/' );
} else {
\EE::warning( 'Challenge Authorization failed. Check logs and check if your domain is pointed correctly to this server.' );
}
\EE::log( 'You can fix the issue and re-run: ee site ssl-verify ' . $domains[0] );

return false;
Expand Down
11 changes: 11 additions & 0 deletions src/helper/class-ee-site.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ abstract class EE_Site_Command {
*/
public $site_meta;

/**
* @var bool $le_renewal_started Whether this process already started an LE renewal (`ssl-renew --all` renews every site in one process).
*/
private static $le_renewal_started = false;

public function __construct() {

$this->fs = new Filesystem();
Expand Down Expand Up @@ -2011,6 +2016,12 @@ private function renew_ssl_cert( $args, $force ) {
return 0;
}

// Space out consecutive renewals to smooth LE API load; sites not due returned above, so they don't wait.
if ( self::$le_renewal_started ) {
sleep( random_int( 1, 5 ) );
}
self::$le_renewal_started = true;

$postfix_exists = \EE_DOCKER::service_exists( 'postfix', $this->site_data['site_fs_path'] );
$containers_to_start = $postfix_exists ? [ 'nginx', 'postfix' ] : [ 'nginx' ];
$this->www_ssl_wrapper( $containers_to_start, false, $force, true );
Expand Down
Loading