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
8 changes: 8 additions & 0 deletions src/Site_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ public function __invoke( $args, $assoc_args ) {
} elseif ( in_array( reset( $args ), [ 'ssl-renew' ], true ) && array_key_exists( 'all', $assoc_args ) ) {
$sites = Site::all();
unset( $assoc_args['all'] );
// Spread per-site dispatches over time to avoid bursting against Let's Encrypt rate limits.
$dispatched = false;
foreach ( $sites as $site ) {
$type = $site->site_type;
$args = [ 'site', 'ssl-renew', $site->site_url ];
Expand All @@ -125,11 +127,17 @@ public function __invoke( $args, $assoc_args ) {
continue;
}

// Jitter between sites only (not before the first / after the last) to smooth burst load on the LE API.
if ( $dispatched ) {
sleep( random_int( 1, 5 ) );
}

$command = EE::get_root_command();
$leaf_command = CommandFactory::create( 'site', $callback, $command );
$command->add_subcommand( 'site', $leaf_command );

EE::run_command( $args, $assoc_args );
$dispatched = true;
}
die;
} else {
Expand Down
42 changes: 39 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 @@ -568,6 +574,26 @@ public function isRenewalNecessary( $domain ) {
return true;
}

/**
* Whether the given exception represents a Let's Encrypt rate-limit response.
*
* Matches the acmephp RateLimitedServerException as well as the 'rateLimited' ACME error
* type / HTTP 429 surfaced in the message, so callers can show rate-limit-specific guidance.
*
* @param \Throwable $e
*
* @return bool
*/
private function is_rate_limit_exception( $e ) {
if ( $e instanceof RateLimitedServerException ) {
return true;
}

$message = strtolower( $e->getMessage() );

return ( false !== strpos( $message, 'ratelimited' ) || false !== strpos( $message, 'too many' ) );
}

/**
* Renew a given domain certificate.
*
Expand Down Expand Up @@ -644,15 +670,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
Loading