diff --git a/src/helper/Site_Letsencrypt.php b/src/helper/Site_Letsencrypt.php index cd1b591c..de533a92 100644 --- a/src/helper/Site_Letsencrypt.php +++ b/src/helper/Site_Letsencrypt.php @@ -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; @@ -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; @@ -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' ); @@ -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. * @@ -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; diff --git a/src/helper/class-ee-site.php b/src/helper/class-ee-site.php index f4dd033b..f7de1f2a 100644 --- a/src/helper/class-ee-site.php +++ b/src/helper/class-ee-site.php @@ -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(); @@ -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 );