rails-letsencrypt 0.11.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0edc7fb113891045af86b87c2804503f58df7c77c3141dd613da62e14f70a41
4
- data.tar.gz: e4049903145539cc020d3568c62d0e92f221909fe1c7696c9b8e72939ca7292e
3
+ metadata.gz: 0bcd52d0b1c063971fe7db79ea96162cdb3895bc8992b190de25773a5d6f3a59
4
+ data.tar.gz: 66ef7517093ba680cd0bdffbdf7dfc2e21f4822a29d41c9d3ba171e2f3cb4299
5
5
  SHA512:
6
- metadata.gz: 885f68a89b22a6ae3abd0aa89b4d4cf868124fef706877b7e8f874212b590c7ed27af2ac1c46c2b9807a224393d60a43b3879609e46b1cf4d0d3e354e1f594bb
7
- data.tar.gz: 4b0c77625b7cbc70fb120cb8554e780304478e17dd8c26fda1f220b630cdad15d98382186db41a1cfc89dcfec5c73f8af1b34632982cfec5178c09161a749822
6
+ metadata.gz: 643bfad8030d1a7962e49ae7a85b354c3c7e13737d3e2e2ce5b9657c0964e20f3faf867cd51b23a2a695d0e1e30e66d7d54a3e125d135b65f2527447fc2d14f9
7
+ data.tar.gz: c37a4c9465a91717846aa952510fd7e37ab006732bb35076c50d3057b7ae28aadd70f0acc12a9c7e9d396a4bafd831762d77e06bb03d74ce22cfccf31fda086b
data/README.md CHANGED
@@ -1,11 +1,16 @@
1
- # LetsEncrypt [![Gem Version](https://badge.fury.io/rb/rails-letsencrypt.svg)](https://badge.fury.io/rb/rails-letsencrypt) [![Code Climate](https://codeclimate.com/github/elct9620/rails-letsencrypt/badges/gpa.svg)](https://codeclimate.com/github/elct9620/rails-letsencrypt)
1
+ Rails LetsEncrypt
2
+ ===
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/rails-letsencrypt.svg)](https://badge.fury.io/rb/rails-letsencrypt)
5
+ [![Code Climate](https://codeclimate.com/github/elct9620/rails-letsencrypt/badges/gpa.svg)](https://codeclimate.com/github/elct9620/rails-letsencrypt)
6
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/elct9620/rails-letsencrypt)
2
7
 
3
8
  Provide manageable Let's Encrypt Certificate for Rails.
4
9
 
5
10
  ## Requirement
6
11
 
7
- * Rails 6.1+
8
- * Ruby 2.7+
12
+ * Rails 7.2+
13
+ * Ruby 3.2+
9
14
 
10
15
  ## Installation
11
16
 
@@ -40,9 +45,13 @@ Add a file to `config/initializers/letsencrypt.rb` and put below config you need
40
45
 
41
46
  ```ruby
42
47
  LetsEncrypt.config do |config|
48
+ # Configure the ACME server
49
+ # Default is Let's Encrypt production server
50
+ # config.acme_server = 'https://acme-v02.api.letsencrypt.org/directory'
51
+
43
52
  # Using Let's Encrypt staging server or not
44
53
  # Default only `Rails.env.production? == true` will use Let's Encrypt production server.
45
- config.use_staging = true
54
+ # config.use_staging = true
46
55
 
47
56
  # Set the private key path
48
57
  # Default is locate at config/letsencrypt.key
@@ -64,14 +73,57 @@ LetsEncrypt.config do |config|
64
73
 
65
74
  # Enable it if you want to customize the model
66
75
  # Default is LetsEncrypt::Certificate
67
- #config.certificate_model = 'MyCertificate'
76
+ # config.certificate_model = 'MyCertificate'
77
+
78
+ # Configure the maximum attempts to re-check status when verifying or issuing
79
+ # config.max_attempts = 30
80
+
81
+ # Configure the interval between attempts
82
+ # config.retry_interval = 1
68
83
  end
69
84
  ```
70
85
 
86
+ > [!WARNING]
87
+ > **Depcrecation Notice**
88
+ > The `use_staging` will be removed in the future, and the `acme_server` will be used to determine the server.
89
+
71
90
  ## Usage
72
91
 
73
92
  The SSL certificate setup depends on the web server, this gem can work with `ngx_mruby` or `kong`.
74
93
 
94
+ ### Service
95
+
96
+ #### Renew Service
97
+
98
+ ```ruby
99
+ certificate = LetsEncrypt::Certificate.find_by(domain: 'example.com')
100
+
101
+ service = LetsEncrypt::RenewService.new
102
+ service.execute(certificate)
103
+ ```
104
+
105
+ #### Verify Service
106
+
107
+ ```ruby
108
+ certificate = LetsEncrypt::Certificate.find_by(domain: 'example.com')
109
+
110
+ order = LetsEncrypt.client.new_order(identifiers: [certificate.domain])
111
+
112
+ service = LetsEncrypt::VerifyService.new
113
+ service.execute(certificate, order)
114
+ ```
115
+
116
+ #### Issue Service
117
+
118
+ ```ruby
119
+ certificate = LetsEncrypt::Certificate.find_by(domain: 'example.com')
120
+
121
+ order = LetsEncrypt.client.new_order(identifiers: [certificate.domain])
122
+
123
+ service = LetsEncrypt::IssueService.new
124
+ service.execute(certificate, order)
125
+ ```
126
+
75
127
  ### Certificate Model
76
128
 
77
129
  #### Create
@@ -83,6 +135,10 @@ cert = LetsEncrypt::Certificate.create(domain: 'example.com')
83
135
  cert.get # alias `verify && issue`
84
136
  ```
85
137
 
138
+ > [!WARNING]
139
+ > **Depcrecation Notice**
140
+ > The `get` will be replaced by `RenewService` in the future.
141
+
86
142
  #### Verify
87
143
 
88
144
  Makes a request to Let's Encrypt and verify domain
@@ -92,6 +148,10 @@ cert = LetsEncrypt::Certificate.find_by(domain: 'example.com')
92
148
  cert.verify
93
149
  ```
94
150
 
151
+ > [!WARNING]
152
+ > **Depcrecation Notice**
153
+ > The `verify` will be replaced by `VerifyService` in the future.
154
+
95
155
  #### Issue
96
156
 
97
157
  Ask Let's Encrypt to issue a new certificate.
@@ -101,6 +161,10 @@ cert = LetsEncrypt::Certificate.find_by(domain: 'example.com')
101
161
  cert.issue
102
162
  ```
103
163
 
164
+ > [!WARNING]
165
+ > **Depcrecation Notice**
166
+ > The `issue` will be replaced by `IssueService` in the future.
167
+
104
168
  #### Renew
105
169
 
106
170
  ```ruby
@@ -108,6 +172,10 @@ cert = LetsEncrypt::Certificate.find_by(domain: 'example.com')
108
172
  cert.renew
109
173
  ```
110
174
 
175
+ > [!WARNING]
176
+ > **Depcrecation Notice**
177
+ > The `renew` will be replaced by `RenewService` in the future.
178
+
111
179
  #### Status
112
180
 
113
181
  Check a certificate is verified and issued.
@@ -136,7 +204,7 @@ rake letsencrypt:renew
136
204
 
137
205
  If you are using Sidekiq or others, you can enqueue renew task daily.
138
206
 
139
- ```
207
+ ```ruby
140
208
  LetsEncrypt::RenewCertificatesJob.perform_later
141
209
  ```
142
210
 
@@ -145,11 +213,17 @@ LetsEncrypt::RenewCertificatesJob.perform_later
145
213
  When the certificate is trying to issue a new one, you can subscribe it for logging or error handling.
146
214
 
147
215
  ```ruby
148
- ActiveSupport::Notifications.subscribe('letsencrypt.issue') do |name, start, finish, id, payload|
149
- Rails.logger.info("Certificate for #{payload[:domain]} is issued")
216
+ ActiveSupport::Notifications.subscribe('letsencrypt.renew') do |name, start, finish, id, payload|
217
+ Rails.logger.info("Certificate for #{payload[:domain]} is renewed")
150
218
  end
151
219
  ```
152
220
 
221
+ The available events are:
222
+
223
+ * `letsencrypt.renew`
224
+ * `letsencrypt.verify`
225
+ * `letsencrypt.issue`
226
+
153
227
  ### ngx_mruby
154
228
 
155
229
  The setup is following this [Article](http://hb.matsumoto-r.jp/entry/2017/03/23/173236)
@@ -200,9 +274,5 @@ server {
200
274
  }
201
275
  ```
202
276
 
203
- ### Kong
204
-
205
- Coming soon.
206
-
207
277
  ## License
208
278
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -6,10 +6,15 @@ module LetsEncrypt
6
6
  queue_as :default
7
7
 
8
8
  def perform
9
- LetsEncrypt.certificate_model.renewable.each do |certificate|
10
- next if certificate.renew
9
+ service = LetsEncrypt::RenewService.new
11
10
 
11
+ LetsEncrypt.certificate_model.renewable.each do |certificate|
12
+ service.execute(certificate)
13
+ rescue LetsEncrypt::MaxCheckExceeded, LetsEncrypt::InvalidStatus
14
+ certificate.update(renew_after: 1.day.from_now)
15
+ rescue Acme::Client::Error => e
12
16
  certificate.update(renew_after: 1.day.from_now)
17
+ Rails.logger.error("LetsEncrypt::RenewCertificatesJob: #{e.message}")
13
18
  end
14
19
  end
15
20
  end
@@ -23,16 +23,13 @@ module LetsEncrypt
23
23
  # index_letsencrypt_certificates_on_renew_after (renew_after)
24
24
  #
25
25
  class Certificate < ApplicationRecord
26
- include CertificateVerifiable
27
- include CertificateIssuable
28
-
29
26
  self.table_name = 'letsencrypt_certificates'
30
27
 
31
28
  validates :domain, presence: true, uniqueness: true
32
29
 
33
30
  scope :active, -> { where('certificate IS NOT NULL AND expires_at > ?', Time.zone.now) }
34
31
  scope :renewable, -> { where('renew_after IS NULL OR renew_after <= ?', Time.zone.now) }
35
- scope :expired, -> { where('expires_at <= ?', Time.zone.now) }
32
+ scope :expired, -> { where(expires_at: ..Time.zone.now) }
36
33
 
37
34
  before_create -> { self.key = OpenSSL::PKey::RSA.new(4096).to_s }
38
35
  after_destroy -> { delete_from_redis }, if: -> { LetsEncrypt.config.use_redis? && active? }
@@ -51,15 +48,6 @@ module LetsEncrypt
51
48
  Time.zone.now >= expires_at
52
49
  end
53
50
 
54
- # Returns true if success get a new certificate
55
- def get
56
- ActiveSupport::Notifications.instrument('letsencrypt.issue', domain: domain) do
57
- verify && issue
58
- end
59
- end
60
-
61
- alias renew get
62
-
63
51
  # Returns full-chain bundled certificates
64
52
  def bundle
65
53
  (certificate || '') + (intermediaries || '')
@@ -73,6 +61,22 @@ module LetsEncrypt
73
61
  @key_object ||= OpenSSL::PKey::RSA.new(key)
74
62
  end
75
63
 
64
+ def challenge!(filename, file_content)
65
+ update!(
66
+ verification_path: filename,
67
+ verification_string: file_content
68
+ )
69
+ end
70
+
71
+ def refresh!(cert, fullchain)
72
+ update!(
73
+ certificate: cert.to_pem,
74
+ intermediaries: fullchain.join("\n\n"),
75
+ expires_at: cert.not_after,
76
+ renew_after: (cert.not_after - 1.month) + rand(10).days
77
+ )
78
+ end
79
+
76
80
  # Save certificate into redis
77
81
  def save_to_redis
78
82
  LetsEncrypt::Redis.save(self)
@@ -83,12 +87,47 @@ module LetsEncrypt
83
87
  LetsEncrypt::Redis.delete(self)
84
88
  end
85
89
 
86
- protected
90
+ # Returns true if success get a new certificate
91
+ def get
92
+ logger.info "Getting certificate for #{domain}"
93
+ service = LetsEncrypt::RenewService.new
94
+ service.execute(self)
95
+ logger.info "Certificate issued for #{domain} " \
96
+ "(expires on #{expires_at}, will renew after #{renew_after})"
97
+
98
+ true
99
+ rescue LetsEncrypt::MaxCheckExceeded, LetsEncrypt::InvalidStatus => e
100
+ logger.error "#{domain}: #{e.message}"
101
+ false
102
+ end
103
+
104
+ alias renew get
87
105
 
88
- def logger
89
- LetsEncrypt.logger
106
+ def verify
107
+ service = LetsEncrypt::VerifyService.new
108
+ service.execute(self, order)
109
+
110
+ true
111
+ rescue LetsEncrypt::MaxCheckExceeded, LetsEncrypt::InvalidStatus => e
112
+ logger.error "#{domain}: #{e.message}"
113
+ false
114
+ end
115
+
116
+ def issue
117
+ logger.info "Getting certificate for #{domain}"
118
+ service = LetsEncrypt::IssueService.new
119
+ service.execute(self, order)
120
+ logger.info "Certificate issued for #{domain} " \
121
+ "(expires on #{expires_at}, will renew after #{renew_after})"
122
+
123
+ true
124
+ rescue LetsEncrypt::MaxCheckExceeded, LetsEncrypt::InvalidStatus => e
125
+ logger.error "#{domain}: #{e.message}"
126
+ false
90
127
  end
91
128
 
129
+ protected
130
+
92
131
  def order
93
132
  @order ||= LetsEncrypt.client.new_order(identifiers: [domain])
94
133
  end
@@ -21,6 +21,10 @@ module LetsEncrypt
21
21
  'db/migrate/create_letsencrypt_certificates.rb'
22
22
  end
23
23
 
24
+ def copy_config
25
+ copy_file 'letsencrypt.rb', 'config/initializers/letsencrypt.rb'
26
+ end
27
+
24
28
  def required_migration_version?
25
29
  Rails::VERSION::MAJOR >= 5
26
30
  end
@@ -0,0 +1,38 @@
1
+ LetsEncrypt.config do |config|
2
+ # Configure the ACME server
3
+ # Default is Let's Encrypt production server
4
+ # config.acme_server = 'https://acme-v02.api.letsencrypt.org/directory'
5
+
6
+ # Using Let's Encrypt staging server or not
7
+ # Default only `Rails.env.production? == true` will use Let's Encrypt production server.
8
+ # config.use_staging = true
9
+
10
+ # Set the private key path
11
+ # Default is locate at config/letsencrypt.key
12
+ config.private_key_path = Rails.root.join('config', 'letsencrypt.key')
13
+
14
+ # Use environment variable to set private key
15
+ # If enable, the API Client will use `LETSENCRYPT_PRIVATE_KEY` as private key
16
+ # Default is false
17
+ config.use_env_key = false
18
+
19
+ # Should sync certificate into redis
20
+ # When using ngx_mruby to dynamic load certificate, this will be helpful
21
+ # Default is false
22
+ config.save_to_redis = false
23
+
24
+ # The redis server url
25
+ # Default is nil
26
+ config.redis_url = 'redis://localhost:6379/1'
27
+
28
+ # Enable it if you want to customize the model
29
+ # Default is LetsEncrypt::Certificate
30
+ # config.certificate_model = 'MyCertificate'
31
+
32
+ # Configure the maximum attempts to re-check status when verifying or issuing
33
+ # config.max_attempts = 30
34
+
35
+ # Configure the interval between attempts
36
+ # config.retry_interval = 1
37
+ end
38
+
@@ -5,6 +5,8 @@ module LetsEncrypt
5
5
  class Configuration
6
6
  include ActiveSupport::Configurable
7
7
 
8
+ config_accessor :acme_directory
9
+
8
10
  config_accessor :use_staging do
9
11
  !Rails.env.production?
10
12
  end
@@ -20,6 +22,14 @@ module LetsEncrypt
20
22
  'LetsEncrypt::Certificate'
21
23
  end
22
24
 
25
+ config_accessor :max_attempts do
26
+ 30
27
+ end
28
+
29
+ config_accessor :retry_interval do
30
+ 1
31
+ end
32
+
23
33
  # Returns true if enabled `save_to_redis` feature
24
34
  def use_redis?
25
35
  save_to_redis == true
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LetsEncrypt
4
+ class Error < StandardError; end
5
+
6
+ class MaxCheckExceeded < Error; end
7
+ class InvalidStatus < Error; end
8
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LetsEncrypt
4
+ # The issue service to download the certificate
5
+ class IssueService
6
+ attr_reader :checker
7
+
8
+ STATUS_PROCESSING = 'processing'
9
+
10
+ def initialize(config: LetsEncrypt.config)
11
+ @checker = StatusChecker.new(
12
+ max_attempts: config.max_attempts,
13
+ interval: config.retry_interval
14
+ )
15
+ end
16
+
17
+ def execute(certificate, order)
18
+ ActiveSupport::Notifications.instrument('letsencrypt.issue', domain: certificate.domain) do
19
+ issue(certificate, order)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def issue(certificate, order)
26
+ csr = build_csr(certificate)
27
+ order.finalize(csr:)
28
+ checker.execute do
29
+ order.reload
30
+ order.status != STATUS_PROCESSING
31
+ end
32
+ fullchain = order.certificate.split("\n\n")
33
+ cert = OpenSSL::X509::Certificate.new(fullchain.shift)
34
+ certificate.refresh!(cert, fullchain)
35
+ end
36
+
37
+ def build_csr(certificate)
38
+ Acme::Client::CertificateRequest.new(
39
+ private_key: OpenSSL::PKey::RSA.new(certificate.key),
40
+ subject: {
41
+ common_name: certificate.domain
42
+ }
43
+ )
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LetsEncrypt
4
+ # The renew service to create or renew the certificate
5
+ class RenewService
6
+ attr_reader :acme_client, :config
7
+
8
+ def initialize(acme_client: LetsEncrypt.client, config: LetsEncrypt.config)
9
+ @acme_client = acme_client
10
+ @config = config
11
+ end
12
+
13
+ def execute(certificate)
14
+ ActiveSupport::Notifications.instrument('letsencrypt.renew', domain: certificate.domain) do
15
+ order = acme_client.new_order(identifiers: [certificate.domain])
16
+
17
+ verify_service = VerifyService.new(config:)
18
+ verify_service.execute(certificate, order)
19
+
20
+ issue_service = IssueService.new(config:)
21
+ issue_service.execute(certificate, order)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LetsEncrypt
4
+ # The status checker to make a loop until the status is reached
5
+ class StatusChecker
6
+ attr_reader :max_attempts, :interval
7
+
8
+ def initialize(max_attempts:, interval:)
9
+ @max_attempts = max_attempts
10
+ @interval = interval
11
+ end
12
+
13
+ def execute
14
+ attempts = 0
15
+
16
+ loop do
17
+ break if yield
18
+
19
+ attempts += 1
20
+ raise MaxCheckExceeded, "Max attempts exceeded (#{max_attempts})" if attempts >= max_attempts
21
+
22
+ sleep interval
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LetsEncrypt
4
+ # Process the verification of the domain
5
+ class VerifyService
6
+ STATUS_PENDING = 'pending'
7
+ STATUS_VALID = 'valid'
8
+
9
+ attr_reader :checker
10
+
11
+ def initialize(config: LetsEncrypt.config)
12
+ @checker = StatusChecker.new(
13
+ max_attempts: config.max_attempts,
14
+ interval: config.retry_interval
15
+ )
16
+ end
17
+
18
+ def execute(certificate, order)
19
+ ActiveSupport::Notifications.instrument('letsencrypt.verify', domain: certificate.domain) do
20
+ verify(certificate, order)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def verify(certificate, order)
27
+ challenge = order.authorizations.first.http
28
+
29
+ certificate.challenge!(challenge.filename, challenge.file_content)
30
+
31
+ challenge.request_validation
32
+
33
+ checker.execute do
34
+ challenge.reload
35
+ challenge.status != STATUS_PENDING
36
+ end
37
+ assert(challenge)
38
+ end
39
+
40
+ def assert(challenge)
41
+ return if challenge.status == STATUS_VALID
42
+
43
+ raise LetsEncrypt::InvalidStatus, "Status not valid (was: #{challenge.status})"
44
+ end
45
+ end
46
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LetsEncrypt
4
- VERSION = '0.11.3'
4
+ VERSION = '0.13.0'
5
5
  end
data/lib/letsencrypt.rb CHANGED
@@ -5,9 +5,13 @@ require 'acme-client'
5
5
  require 'redis'
6
6
  require 'letsencrypt/railtie'
7
7
  require 'letsencrypt/engine'
8
+ require 'letsencrypt/errors'
8
9
  require 'letsencrypt/configuration'
9
- require 'letsencrypt/logger_proxy'
10
10
  require 'letsencrypt/redis'
11
+ require 'letsencrypt/status_checker'
12
+ require 'letsencrypt/verify_service'
13
+ require 'letsencrypt/issue_service'
14
+ require 'letsencrypt/renew_service'
11
15
 
12
16
  # :nodoc:
13
17
  module LetsEncrypt
@@ -22,8 +26,9 @@ module LetsEncrypt
22
26
  # Create the ACME Client to Let's Encrypt
23
27
  def client
24
28
  @client ||= ::Acme::Client.new(
25
- private_key: private_key,
26
- directory: directory
29
+ private_key:,
30
+ directory:,
31
+ bad_nonce_retry: 5
27
32
  )
28
33
  end
29
34
 
@@ -40,7 +45,8 @@ module LetsEncrypt
40
45
 
41
46
  # Get current using Let's Encrypt endpoint
42
47
  def directory
43
- @directory ||= config.use_staging? ? ENDPOINT_STAGING : ENDPOINT
48
+ @directory ||= config.acme_directory ||
49
+ (config.use_staging? ? ENDPOINT_STAGING : ENDPOINT)
44
50
  end
45
51
 
46
52
  # Register a Let's Encrypt account
@@ -68,7 +74,7 @@ module LetsEncrypt
68
74
  end
69
75
 
70
76
  def logger
71
- @logger ||= LoggerProxy.new(Rails.logger, tags: ['LetsEncrypt'])
77
+ @logger ||= Rails.logger.tagged('LetsEncrypt')
72
78
  end
73
79
 
74
80
  # Config how to Let's Encrypt works for Rails
@@ -77,9 +83,9 @@ module LetsEncrypt
77
83
  # # Always use production mode to connect Let's Encrypt API server
78
84
  # config.use_staging = false
79
85
  # end
80
- def config(&block)
86
+ def config(&)
81
87
  @config ||= Configuration.new
82
- instance_exec(@config, &block) if block_given?
88
+ instance_exec(@config, &) if block_given?
83
89
  @config
84
90
  end
85
91
 
@@ -3,18 +3,21 @@
3
3
  namespace :letsencrypt do
4
4
  desc 'Renew certificates that already expired or expiring soon'
5
5
  task renew: :environment do
6
- count = 0
6
+ success = 0
7
7
  failed = 0
8
8
 
9
- LetsEncrypt.certificate_model.renewable.each do |certificate|
10
- count += 1
11
-
12
- next if certificate.renew
9
+ service = LetsEncrypt::RenewService.new
13
10
 
11
+ LetsEncrypt.certificate_model.renewable.each do |certificate|
12
+ service.execute(certificate)
13
+ success += 1
14
+ rescue Acme::Client::Error, LetsEncrypt::MaxCheckExceeded, LetsEncrypt::InvalidStatus => e
14
15
  failed += 1
15
- puts "Could not renew domain: #{certificate.domain}"
16
+ puts "Could not renew domain: #{certificate.domain} - #{e.message}"
16
17
  end
17
18
 
18
- puts "Renewed #{count - failed} out of #{count} domains"
19
+ puts "Renewed #{success} certificates successfully."
20
+ puts "Failed to renew #{failed} certificates."
21
+ puts "Total: #{success + failed} certificates."
19
22
  end
20
23
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-letsencrypt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.3
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - 蒼時弦也
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-02-13 00:00:00.000000000 Z
10
+ date: 2025-05-20 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: acme-client
@@ -143,26 +142,28 @@ executables: []
143
142
  extensions: []
144
143
  extra_rdoc_files: []
145
144
  files:
146
- - MIT-LICENSE
147
145
  - README.md
148
146
  - Rakefile
149
147
  - app/controllers/lets_encrypt/application_controller.rb
150
148
  - app/controllers/lets_encrypt/verifications_controller.rb
151
149
  - app/jobs/lets_encrypt/application_job.rb
152
150
  - app/jobs/lets_encrypt/renew_certificates_job.rb
153
- - app/models/concerns/lets_encrypt/certificate_issuable.rb
154
- - app/models/concerns/lets_encrypt/certificate_verifiable.rb
155
151
  - app/models/lets_encrypt/certificate.rb
156
152
  - config/routes.rb
157
153
  - lib/generators/lets_encrypt/install_generator.rb
158
154
  - lib/generators/lets_encrypt/register_generator.rb
155
+ - lib/generators/lets_encrypt/templates/letsencrypt.rb
159
156
  - lib/generators/lets_encrypt/templates/migration.rb
160
157
  - lib/letsencrypt.rb
161
158
  - lib/letsencrypt/configuration.rb
162
159
  - lib/letsencrypt/engine.rb
163
- - lib/letsencrypt/logger_proxy.rb
160
+ - lib/letsencrypt/errors.rb
161
+ - lib/letsencrypt/issue_service.rb
164
162
  - lib/letsencrypt/railtie.rb
165
163
  - lib/letsencrypt/redis.rb
164
+ - lib/letsencrypt/renew_service.rb
165
+ - lib/letsencrypt/status_checker.rb
166
+ - lib/letsencrypt/verify_service.rb
166
167
  - lib/letsencrypt/version.rb
167
168
  - lib/rails-letsencrypt.rb
168
169
  - lib/tasks/letsencrypt_tasks.rake
@@ -171,7 +172,6 @@ licenses:
171
172
  - MIT
172
173
  metadata:
173
174
  rubygems_mfa_required: 'true'
174
- post_install_message:
175
175
  rdoc_options: []
176
176
  require_paths:
177
177
  - lib
@@ -179,15 +179,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
179
179
  requirements:
180
180
  - - ">="
181
181
  - !ruby/object:Gem::Version
182
- version: 2.7.0
182
+ version: 3.2.0
183
183
  required_rubygems_version: !ruby/object:Gem::Requirement
184
184
  requirements:
185
185
  - - ">="
186
186
  - !ruby/object:Gem::Version
187
187
  version: '0'
188
188
  requirements: []
189
- rubygems_version: 3.1.6
190
- signing_key:
189
+ rubygems_version: 3.6.2
191
190
  specification_version: 4
192
191
  summary: The Let's Encrypt certificate manager for rails
193
192
  test_files: []
data/MIT-LICENSE DELETED
@@ -1,20 +0,0 @@
1
- Copyright 2017 蒼時弦也
2
-
3
- Permission is hereby granted, free of charge, to any person obtaining
4
- a copy of this software and associated documentation files (the
5
- "Software"), to deal in the Software without restriction, including
6
- without limitation the rights to use, copy, modify, merge, publish,
7
- distribute, sublicense, and/or sell copies of the Software, and to
8
- permit persons to whom the Software is furnished to do so, subject to
9
- the following conditions:
10
-
11
- The above copyright notice and this permission notice shall be
12
- included in all copies or substantial portions of the Software.
13
-
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LetsEncrypt
4
- # :nodoc:
5
- module CertificateIssuable
6
- extend ActiveSupport::Concern
7
-
8
- # Returns true if issue new certificate succeed.
9
- def issue
10
- logger.info "Getting certificate for #{domain}"
11
- create_certificate
12
- logger.info "Certificate issued for #{domain} (expires on #{expires_at}, will renew after #{renew_after})"
13
- true
14
- end
15
-
16
- private
17
-
18
- def csr
19
- Acme::Client::CertificateRequest.new(
20
- private_key: OpenSSL::PKey::RSA.new(key),
21
- subject: {
22
- common_name: domain
23
- }
24
- )
25
- end
26
-
27
- def create_certificate
28
- order.finalize(csr: csr)
29
- sleep 1 while order.status == 'processing'
30
- fullchain = order.certificate.split("\n\n")
31
- assign_new_certificate(fullchain)
32
- save!
33
- end
34
-
35
- def assign_new_certificate(fullchain)
36
- cert = OpenSSL::X509::Certificate.new(fullchain.shift)
37
- self.certificate = cert.to_pem
38
- self.intermediaries = fullchain.join("\n\n")
39
- self.expires_at = cert.not_after
40
- self.renew_after = (expires_at - 1.month) + rand(10).days
41
- end
42
- end
43
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LetsEncrypt
4
- # :nodoc:
5
- module CertificateVerifiable
6
- extend ActiveSupport::Concern
7
-
8
- # Returns true if verify domain is succeed.
9
- def verify
10
- create_order
11
- start_challenge
12
- wait_verify_status
13
- check_verify_status
14
- rescue Acme::Client::Error => e
15
- retry_on_verify_error(e)
16
- end
17
-
18
- private
19
-
20
- def create_order
21
- # TODO: Support multiple domain
22
- @challenge = order.authorizations.first.http
23
- self.verification_path = @challenge.filename
24
- self.verification_string = @challenge.file_content
25
- save!
26
- end
27
-
28
- def start_challenge
29
- logger.info "Attempting verification of #{domain}"
30
- @challenge.request_validation
31
- end
32
-
33
- def wait_verify_status
34
- checks = 0
35
- until @challenge.status != 'pending'
36
- checks += 1
37
- if checks > 30
38
- logger.info "#{domain}: Status remained at pending for 30 checks"
39
- return false
40
- end
41
- sleep 1
42
- @challenge.reload
43
- end
44
- end
45
-
46
- def check_verify_status
47
- unless @challenge.status == 'valid'
48
- logger.info "#{domain}: Status was not valid (was: #{@challenge.status})"
49
- return false
50
- end
51
-
52
- true
53
- end
54
-
55
- def retry_on_verify_error(error)
56
- @retries ||= 0
57
- if error.is_a?(Acme::Client::Error::BadNonce) && @retries < 5
58
- @retries += 1
59
- logger.info "#{domain}: Bad nounce encountered. Retrying (#{@retries} of 5 attempts)"
60
- sleep 1
61
- verify
62
- else
63
- logger.info "#{domain}: Error: #{error.class} (#{error.message})"
64
- false
65
- end
66
- end
67
- end
68
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LetsEncrypt
4
- # :nodoc:
5
- class LoggerProxy
6
- attr_reader :tags
7
-
8
- def initialize(logger, tags:)
9
- @logger = logger
10
- @tags = tags.flatten
11
- end
12
-
13
- def add_tags(*tags)
14
- @tags += tags.flatten
15
- @tags = @tags.uniq
16
- end
17
-
18
- def tag(logger, &block)
19
- if logger.respond_to?(:tagged)
20
- current_tags = tags - logger.formatter.current_tags
21
- logger.tagged(*current_tags, &block)
22
- else
23
- yield
24
- end
25
- end
26
-
27
- %i[debug info warn error fatal unknown].each do |severity|
28
- define_method(severity) do |message|
29
- log severity, message
30
- end
31
- end
32
-
33
- private
34
-
35
- def log(type, message)
36
- tag(@logger) { @logger.send type, message }
37
- end
38
- end
39
- end