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
37 changes: 36 additions & 1 deletion src/helper/Site_Letsencrypt.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use AcmePhp\Core\Exception\AcmeCoreServerException;
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 @@ -209,7 +210,12 @@ public function authorize( Array $domains, $wildcard = false, $preferred_challen
try {
$order = $this->client->requestOrder( $domains );
} catch ( \Exception $e ) {
\EE::warning( 'Let\'s Encrypt order request failed (' . $e->getMessage() . '). 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 ) . ' (' . $e->getMessage() . '). Please wait before retrying. Ref: https://letsencrypt.org/docs/rate-limits/' );
} else {
\EE::warning( 'Let\'s Encrypt order request failed (' . $e->getMessage() . '). 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 @@ -280,6 +286,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 @@ -686,6 +697,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 @@ -762,12 +789,20 @@ private function executeRenewal( $domain, array $alternativeNames, $email, $forc
} catch ( \Exception $e ) {
\EE::warning( 'A critical error occurred during certificate renewal: ' . $e->getMessage() );
\EE::debug( print_r( $e, true ) );
// A rate limit is not a misconfigured-domain failure; point the user to the LE rate-limit docs.
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/' );
}
\EE::log( 'You can fix the issue and re-run: ee site ssl-verify ' . $domain );

return false;
} catch ( \Throwable $e ) {
\EE::warning( 'A critical error occurred during certificate renewal: ' . $e->getMessage() );
\EE::debug( print_r( $e, true ) );
// A rate limit is not a misconfigured-domain failure; point the user to the LE rate-limit docs.
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/' );
}
\EE::log( 'You can fix the issue and re-run: ee site ssl-verify ' . $domain );

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 @@ -76,6 +76,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 @@ -2172,6 +2177,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