letscert 0.4.1 → 0.4.2

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.
@@ -0,0 +1,29 @@
1
+ module LetsCert
2
+
3
+ # Account key IO plugin
4
+ # @author Sylvain Daubert
5
+ class AccountKey < IOPlugin
6
+ include FileIOPluginMixin
7
+ include JWKIOPluginMixin
8
+
9
+ # @return [Hash] always get +true+ for +:account_key+ key
10
+ def persisted
11
+ { account_key: true }
12
+ end
13
+
14
+ # @return [Hash]
15
+ def load_from_content(content)
16
+ { account_key: load_jwk(content) }
17
+ end
18
+
19
+ # Save account key.
20
+ # @param [Hash] data
21
+ # @return [void]
22
+ def save(data)
23
+ save_to_file(dump_jwk(data[:account_key]))
24
+ end
25
+
26
+ end
27
+
28
+ IOPlugin.register(AccountKey, 'account_key.json')
29
+ end
@@ -0,0 +1,29 @@
1
+ module LetsCert
2
+
3
+ # Cert file plugin
4
+ # @author Sylvain Daubert
5
+ class CertFile < OpenSSLIOPlugin
6
+ include FileIOPluginMixin
7
+
8
+ # @return [Hash] always get +true+ for +:cert+ key
9
+ def persisted
10
+ @persisted ||= { cert: true }
11
+ end
12
+
13
+ # @return [Hash]
14
+ def load_from_content(content)
15
+ { cert: load_cert(content) }
16
+ end
17
+
18
+ # Save certificate.
19
+ # @param [Hash] data
20
+ # @return [void]
21
+ def save(data)
22
+ save_to_file(dump_cert(data[:cert]))
23
+ end
24
+
25
+ end
26
+
27
+ IOPlugin.register(CertFile, 'cert.pem', :pem)
28
+ IOPlugin.register(CertFile, 'cert.der', :der)
29
+ end
@@ -0,0 +1,32 @@
1
+ module LetsCert
2
+
3
+ # Chain file plugin
4
+ # @author Sylvain Daubert
5
+ class ChainFile < OpenSSLIOPlugin
6
+ include FileIOPluginMixin
7
+
8
+ # @return [Hash] always get +true+ for +:chain+ key
9
+ def persisted
10
+ @persisted ||= { chain: true }
11
+ end
12
+
13
+ # @return [Hash]
14
+ def load_from_content(content)
15
+ chain = []
16
+ split_pems(content) do |pem|
17
+ chain << load_cert(pem)
18
+ end
19
+ { chain: chain }
20
+ end
21
+
22
+ # Save chain.
23
+ # @param [Hash] data
24
+ # @return [void]
25
+ def save(data)
26
+ save_to_file(data[:chain].map { |c| dump_cert(c) }.join)
27
+ end
28
+
29
+ end
30
+
31
+ IOPlugin.register(ChainFile, 'chain.pem', :pem)
32
+ end
@@ -0,0 +1,48 @@
1
+ module LetsCert
2
+
3
+ # Mixin for IOPmugin subclasses that handle files
4
+ # @author Sylvain Daubert
5
+ module FileIOPluginMixin
6
+
7
+ # Load data from file named +#name+
8
+ # @return [Hash]
9
+ def load
10
+ logger.debug { "Loading #{@name}" }
11
+
12
+ begin
13
+ content = File.read(@name)
14
+ rescue Errno::ENOENT => ex
15
+ logger.info { "no #{@name} file" }
16
+ return self.class.empty_data
17
+ end
18
+
19
+ load_from_content(content)
20
+ end
21
+
22
+ # @abstract
23
+ # @param [String] _content
24
+ # @return [Hash]
25
+ def load_from_content(_content)
26
+ raise NotImplementedError
27
+ end
28
+
29
+ # Save data to file +#name+
30
+ # @param [Hash] data
31
+ # @return [void]
32
+ def save_to_file(data)
33
+ return if data.nil?
34
+
35
+ logger.info { "saving #{@name}" }
36
+ begin
37
+ File.open(name, 'w') do |f|
38
+ f.write(data)
39
+ end
40
+ rescue Errno => ex
41
+ @logger.error { ex.message }
42
+ raise Error, "Error when saving #{@name}"
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,39 @@
1
+ module LetsCert
2
+
3
+ # Fullchain file plugin
4
+ # @author Sylvain Daubert
5
+ class FullChainFile < ChainFile
6
+
7
+ # @return [Hash] always get +true+ for +:cert+ and +:chain+ keys
8
+ def persisted
9
+ @persisted ||= { cert: true, chain: true }
10
+ end
11
+
12
+ # Load full certificate chain
13
+ # @return [Hash]
14
+ def load
15
+ data = super
16
+ if data[:chain].nil? or data[:chain].empty?
17
+ cert = nil
18
+ chain = []
19
+ else
20
+ cert = data[:chain].shift
21
+ chain = data[:chain]
22
+ end
23
+
24
+ { account_key: data[:account_key], key: data[:key], cert: cert,
25
+ chain: chain }
26
+ end
27
+
28
+ # Save fullchain.
29
+ # @param [Hash] data
30
+ # @return [void]
31
+ def save(data)
32
+ super(account_key: data[:account_key], key: data[:key], cert: nil,
33
+ chain: [data[:cert]] + data[:chain])
34
+ end
35
+
36
+ end
37
+
38
+ IOPlugin.register(FullChainFile, 'fullchain.pem', :pem)
39
+ end
@@ -0,0 +1,68 @@
1
+ require 'base64'
2
+
3
+ module LetsCert
4
+
5
+ # Mixin for IOPlugin subclasses that handle JWK
6
+ # @author Sylvain Daubert
7
+ module JWKIOPluginMixin
8
+
9
+ # Encode string +data+ to base64
10
+ # @param [String] data
11
+ # @return [String]
12
+ def urlsafe_encode64(data)
13
+ Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '')
14
+ end
15
+
16
+ # Decode base64 string +data+
17
+ # @param [String] data
18
+ # @return [String]
19
+ def urlsafe_decode64(data)
20
+ Base64.urlsafe_decode64(data.sub(/[\s=]*\z/, ''))
21
+ end
22
+
23
+ # Load crypto data from JSON-encoded file
24
+ # @param [String] data JSON-encoded data
25
+ # @return [OpenSSL::PKey::PKey]
26
+ def load_jwk(data)
27
+ return nil if data.empty?
28
+
29
+ h = JSON.parse(data)
30
+ case h['kty']
31
+ when 'RSA'
32
+ pkey = OpenSSL::PKey::RSA.new
33
+ %w(e n d p q).collect do |key|
34
+ next if h[key].nil?
35
+ value = OpenSSL::BN.new(urlsafe_decode64(h[key]), 2)
36
+ pkey.send "#{key}=".to_sym, value
37
+ end
38
+ else
39
+ raise Error, "unknown account key type '#{k['kty']}'"
40
+ end
41
+
42
+ pkey
43
+ end
44
+
45
+ # Dump crypto data (key) to a JSON-encoded string
46
+ # @param [OpenSSL::PKey] key
47
+ # @return [String]
48
+ def dump_jwk(key)
49
+ return {}.to_json if key.nil?
50
+
51
+ h = { 'kty' => 'RSA' }
52
+ case key
53
+ when OpenSSL::PKey::RSA
54
+ h['e'] = urlsafe_encode64(key.e.to_s(2)) if key.e
55
+ h['n'] = urlsafe_encode64(key.n.to_s(2)) if key.n
56
+ if key.private?
57
+ h['d'] = urlsafe_encode64(key.d.to_s(2))
58
+ h['p'] = urlsafe_encode64(key.p.to_s(2))
59
+ h['q'] = urlsafe_encode64(key.q.to_s(2))
60
+ end
61
+ else
62
+ raise Error, 'only RSA keys are supported'
63
+ end
64
+ h.to_json
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,29 @@
1
+ module LetsCert
2
+
3
+ # Key file plugin
4
+ # @author Sylvain Daubert
5
+ class KeyFile < OpenSSLIOPlugin
6
+ include FileIOPluginMixin
7
+
8
+ # @return [Hash] always get +true+ for +:key+ key
9
+ def persisted
10
+ @persisted ||= { key: true }
11
+ end
12
+
13
+ # @return [Hash]
14
+ def load_from_content(content)
15
+ { key: load_key(content) }
16
+ end
17
+
18
+ # Save private key.
19
+ # @param [Hash] data
20
+ # @return [void]
21
+ def save(data)
22
+ save_to_file(dump_key(data[:key]))
23
+ end
24
+
25
+ end
26
+
27
+ IOPlugin.register(KeyFile, 'key.pem', :pem)
28
+ IOPlugin.register(KeyFile, 'key.der', :der)
29
+ end
@@ -0,0 +1,68 @@
1
+ module LetsCert
2
+
3
+ # OpenSSL IOPlugin
4
+ # @author Sylvain Daubert
5
+ class OpenSSLIOPlugin < IOPlugin
6
+
7
+ # @private Regular expression to discriminate PEM
8
+ PEM_RE = /^-----BEGIN CERTIFICATE-----\n.*?\n-----END CERTIFICATE-----\n/m
9
+
10
+ # @param [String] name filename
11
+ # @param [:pem,:der] type
12
+ def initialize(name, type)
13
+ case type
14
+ when :pem
15
+ when :der
16
+ else
17
+ raise ArgumentError, 'type should be :pem or :der'
18
+ end
19
+
20
+ @type = type
21
+ super(name)
22
+ end
23
+
24
+ # Load key from raw +data+
25
+ # @param [String] data
26
+ # @return [OpenSSL::PKey]
27
+ def load_key(data)
28
+ OpenSSL::PKey::RSA.new data
29
+ end
30
+
31
+ # Dump key/cert data
32
+ # @param [OpenSSL::PKey] key
33
+ # @return [String]
34
+ def dump_key(key)
35
+ case @type
36
+ when :pem
37
+ key.to_pem
38
+ when :der
39
+ key.to_der
40
+ end
41
+ end
42
+ alias dump_cert dump_key
43
+
44
+ # Load certificate from raw +data+
45
+ # @param [String] data
46
+ # @return [OpenSSL::X509::Certificate]
47
+ def load_cert(data)
48
+ OpenSSL::X509::Certificate.new data
49
+ end
50
+
51
+ private
52
+
53
+ # Split concatenated PEMs.
54
+ # @param [String] data
55
+ # @yield [String] pem
56
+ def split_pems(data)
57
+ my_data = data
58
+ m = my_data.match(PEM_RE)
59
+ while m
60
+ yield m[0]
61
+ my_data = my_data[m.end(0)..-1]
62
+ m = my_data.match(PEM_RE)
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -41,8 +41,8 @@ module LetsCert
41
41
  module ClassMethods
42
42
 
43
43
  # @private hook called when a subclass is created.
44
- # Take care of all subclasses to later properly set @logger class instance
45
- # variable.
44
+ # Take care of all subclasses to later properly set @logger class
45
+ # instance variable.
46
46
  # @param [Class] subclass
47
47
  # @return [void]
48
48
  def inherited(subclass)
@@ -69,5 +69,4 @@ module LetsCert
69
69
  end
70
70
 
71
71
  end
72
-
73
72
  end
@@ -25,86 +25,15 @@ require 'fileutils'
25
25
 
26
26
  require_relative 'io_plugin'
27
27
  require_relative 'certificate'
28
+ require_relative 'runner/logger_formatter'
29
+ require_relative 'runner/valid_time'
28
30
 
29
31
  module LetsCert
30
32
 
31
33
  # Runner class: analyse and execute CLI commands.
32
34
  # @author Sylvain Daubert
35
+ # rubocop:disable Metrics/ClassLength
33
36
  class Runner
34
- # Get options
35
- # @return [Hash]
36
- attr_reader :options
37
- # @return [Logger]
38
- attr_accessor :logger
39
-
40
- # Custom logger formatter
41
- class LoggerFormatter < Logger::Formatter
42
-
43
- # @private log format
44
- FORMAT = "[%s] %5s: %s\n"
45
-
46
- # @param [String] severity
47
- # @param [Datetime] time
48
- # @param [nil,String] progname
49
- # @param [String] msg
50
- # @return [String]
51
- def call(severity, time, progname, msg)
52
- FORMAT % [format_datetime(time), severity, msg2str(msg)]
53
- end
54
-
55
-
56
- private
57
-
58
- # @private simple datetime formatter
59
- # @param [DateTime] time
60
- # @return [String]
61
- def format_datetime(time)
62
- time.strftime("%Y-%m-%d %H:%M:%S")
63
- end
64
-
65
- end
66
-
67
- # Class used to process validation time from String.
68
- # @author Sylvain Daubert
69
- class ValidTime
70
-
71
- # @param [String] str time string. May be:
72
- # * an integer -> time in seconds
73
- # * an integer plus a letter:
74
- # * 30m: 30 minutes,
75
- # * 30h: 30 hours,
76
- # * 30d: 30 days.
77
- def initialize(str)
78
- m = str.match(/^(\d+)([mhd])?$/)
79
- if m
80
- case m[2]
81
- when nil
82
- @seconds = m[1].to_i
83
- when 'm'
84
- @seconds = m[1].to_i * 60
85
- when 'h'
86
- @seconds = m[1].to_i * 60 * 60
87
- when 'd'
88
- @seconds = m[1].to_i * 24 * 60 * 60
89
- end
90
- else
91
- raise OptionParser::InvalidArgument, "invalid argument: --valid-min #{str}"
92
- end
93
- @string = str
94
- end
95
-
96
- # Get time in seconds
97
- # @return [Integer]
98
- def to_seconds
99
- @seconds
100
- end
101
-
102
- # Get time as string
103
- # @return [String]
104
- def to_s
105
- @string
106
- end
107
- end
108
37
 
109
38
  # Exit value for OK
110
39
  RETURN_OK = 1
@@ -112,9 +41,12 @@ module LetsCert
112
41
  RETURN_OK_CERT = 0
113
42
  # Exit value for error(s)
114
43
  RETURN_ERROR = 2
115
-
44
+
45
+ # Get options
46
+ # @return [Hash]
47
+ attr_reader :options
116
48
  # @return [Logger]
117
- attr_reader :logger
49
+ attr_accessor :logger
118
50
 
119
51
  # Run LetsCert
120
52
  # @return [Integer]
@@ -125,7 +57,6 @@ module LetsCert
125
57
  runner.run
126
58
  end
127
59
 
128
-
129
60
  def initialize
130
61
  @options = {
131
62
  verbose: 0,
@@ -134,9 +65,9 @@ module LetsCert
134
65
  cert_key_size: 2048,
135
66
  valid_min: ValidTime.new('30d'),
136
67
  account_key_size: 4096,
137
- tos_sha256: '33d233c8ab558ba6c8ebc370a509acdded8b80e5d587aa5d192193f35226540f',
138
- user_agent: "letscert/#{VERSION.gsub(/\..*/, '')}",
139
- server: 'https://acme-v01.api.letsencrypt.org/directory',
68
+ tos_sha256: '33d233c8ab558ba6c8ebc370a509acdded8b80e5d587aa5d192193f3' \
69
+ '5226540f',
70
+ server: 'https://acme-v01.api.letsencrypt.org/directory'
140
71
  }
141
72
 
142
73
  @logger = Logger.new($stdout)
@@ -148,62 +79,19 @@ module LetsCert
148
79
  # * 1 if renewal was not necessery
149
80
  # * 2 in case of errors
150
81
  def run
151
- if @options[:print_help]
152
- puts @opt_parser
153
- exit RETURN_OK
154
- end
155
-
156
- if @options[:show_version]
157
- puts "letscert #{LetsCert::VERSION}"
158
- puts "Copyright (c) 2016 Sylvain Daubert"
159
- puts "License MIT: see http://opensource.org/licenses/MIT"
160
- exit RETURN_OK
161
- end
162
-
163
- case @options[:verbose]
164
- when 0
165
- @logger.level = Logger::Severity::WARN
166
- when 1
167
- @logger.level = Logger::Severity::INFO
168
- when 2..5
169
- @logger.level = Logger::Severity::DEBUG
170
- end
171
-
172
- @logger.debug { "options are: #{@options.inspect}" }
173
-
174
- IOPlugin.logger = @logger
175
- Certificate.logger = @logger
82
+ print_help_if_needed
83
+ show_version_if_needed
84
+ set_logger_level
85
+ set_logger
176
86
 
177
87
  begin
178
- if @options[:domains].empty?
179
- raise Error, "At leat one domain must be given with --domain option.\n" +
180
- "Try 'letscert --help' for more information."
181
- end
182
-
88
+ check_domains
183
89
  if @options[:revoke]
184
- data = load_data_from_disk(IOPlugin.registered.keys)
185
- certificate = Certificate.new(data[:cert])
186
- if certificate.revoke(data[:account_key], @options)
187
- RETURN_OK
188
- else
189
- RETURN_ERROR
190
- end
90
+ revoke
191
91
  else
192
92
  check_persisted
193
-
194
- data = load_data_from_disk(@options[:files])
195
-
196
- certificate = Certificate.new(data[:cert])
197
- if certificate.valid?(@options[:domains], @options[:valid_min].to_seconds)
198
- @logger.info { 'no need to update cert' }
199
- RETURN_OK
200
- else
201
- # update/create cert
202
- certificate.get data[:account_key], data[:key], @options
203
- RETURN_OK_CERT
204
- end
93
+ get_certificate
205
94
  end
206
-
207
95
  rescue Error, Acme::Client::Error => ex
208
96
  msg = ex.message
209
97
  msg = "[Acme] #{msg}" if ex.is_a?(Acme::Client::Error)
@@ -213,13 +101,13 @@ module LetsCert
213
101
  end
214
102
  end
215
103
 
216
-
217
104
  # Parse line command options
218
105
  # @raise [OptionParser::InvalidOption] on unrecognized or malformed option
219
106
  # @return [void]
107
+ # rubocop:disable Metrics/MethodLength
220
108
  def parse_options
221
109
  @opt_parser = OptionParser.new do |opts|
222
- opts.banner = "Usage: lestcert [options]"
110
+ opts.banner = 'Usage: lestcert [options]'
223
111
 
224
112
  opts.separator('')
225
113
 
@@ -229,8 +117,9 @@ module LetsCert
229
117
  opts.on('-V', '--version', 'Show version and exit') do |v|
230
118
  @options[:show_version] = v
231
119
  end
232
- opts.on('-v', '--verbose', 'Run verbosely') { |v| @options[:verbose] += 1 if v }
233
-
120
+ opts.on('-v', '--verbose', 'Run verbosely') do |v|
121
+ @options[:verbose] += 1 if v
122
+ end
234
123
 
235
124
  opts.separator("\nWebroot manager:")
236
125
 
@@ -252,7 +141,7 @@ module LetsCert
252
141
  @options[:revoke] = revoke
253
142
  end
254
143
 
255
- opts.on("-f", "--file FILE", 'Input/output file.',
144
+ opts.on('-f', '--file FILE', 'Input/output file.',
256
145
  'Can be specified multiple times',
257
146
  'Allowed values: account_key.json, cert.der,',
258
147
  'cert.pem, chain.pem, full.pem,',
@@ -269,9 +158,10 @@ module LetsCert
269
158
  opts.accept(ValidTime) do |valid_time|
270
159
  ValidTime.new(valid_time)
271
160
  end
272
- opts.on('--valid-min TIME', ValidTime, 'Renew existing certificate if validity',
161
+ opts.on('--valid-min TIME', ValidTime,
162
+ 'Renew existing certificate if validity',
273
163
  'is lesser than TIME',
274
- "(default: #{@options[:valid_min].to_s})") do |vt|
164
+ "(default: #{@options[:valid_min]})") do |vt|
275
165
  @options[:valid_min] = vt
276
166
  end
277
167
 
@@ -280,12 +170,13 @@ module LetsCert
280
170
  end
281
171
 
282
172
  opts.separator("\nRegistration:")
283
- opts.separator(" Automatically register an account with he ACME CA specified" +
284
- " by --server")
173
+ opts.separator(' Automatically register an account with he ACME CA' \
174
+ ' specified by --server')
285
175
  opts.separator('')
286
176
 
287
177
  opts.on('--account-key-size BITS', Integer,
288
- "Account key size (default: #{@options[:account_key_size]})") do |bits|
178
+ 'Account key size (default: ' \
179
+ "#{@options[:account_key_size]})") do |bits|
289
180
  @options[:account_key_size] = bits
290
181
  end
291
182
 
@@ -307,11 +198,6 @@ module LetsCert
307
198
  opts.separator(' Configure properties of HTTP requests and responses.')
308
199
  opts.separator('')
309
200
 
310
- opts.on('--user-agent NAME', 'User-Agent sent in all HTTP requests',
311
- "(default: #{@options[:user_agent]})") do |ua|
312
- @options[:user_agent] = ua
313
- end
314
-
315
201
  opts.on('--server URI', 'URI for the CA ACME API endpoint',
316
202
  "(default: #{@options[:server]})") do |uri|
317
203
  @options[:server] = uri
@@ -325,14 +211,8 @@ module LetsCert
325
211
  # Check all components are covered by plugins
326
212
  # @raise [Error]
327
213
  def check_persisted
328
- persisted = IOPlugin.empty_data
329
-
330
- @options[:files].each do |file|
331
- persisted.merge!(IOPlugin.registered[file].persisted) do |k, oldv, newv|
332
- oldv || newv
333
- end
334
- end
335
- not_persisted = persisted.keys.find_all { |k| !persisted[k] }
214
+ persisted = persisted_data
215
+ not_persisted = persisted.keys.find_all { |k| persisted[k].nil? }
336
216
 
337
217
  unless not_persisted.empty?
338
218
  raise Error, 'Selected IO plugins do not cover following components: ' +
@@ -340,9 +220,90 @@ module LetsCert
340
220
  end
341
221
  end
342
222
 
343
-
344
223
  private
345
224
 
225
+ # Print help and exit, if +:print_help+ option is set
226
+ # @return [void]
227
+ # rubocop:disable Style/GuardClause
228
+ def print_help_if_needed
229
+ if @options[:print_help]
230
+ puts @opt_parser
231
+ exit RETURN_OK
232
+ end
233
+ end
234
+
235
+ # Show version and exit, if +:show_version+ option is set
236
+ # @return [void]
237
+ def show_version_if_needed
238
+ if @options[:show_version]
239
+ puts "letscert #{LetsCert::VERSION}"
240
+ puts 'Copyright (c) 2016 Sylvain Daubert'
241
+ puts 'License MIT: see http://opensource.org/licenses/MIT'
242
+ exit RETURN_OK
243
+ end
244
+ end
245
+
246
+ # Set logger level from +:verbose+ option
247
+ # @return [void]
248
+ def set_logger_level
249
+ case @options[:verbose]
250
+ when 0
251
+ @logger.level = Logger::Severity::WARN
252
+ when 1
253
+ @logger.level = Logger::Severity::INFO
254
+ when 2..5
255
+ @logger.level = Logger::Severity::DEBUG
256
+ end
257
+ end
258
+
259
+ # Set logger for IOPlugin and Certificate classes.
260
+ # @return [void]
261
+ def set_logger
262
+ @logger.debug { "options are: #{@options.inspect}" }
263
+ IOPlugin.logger = @logger
264
+ Certificate.logger = @logger
265
+ end
266
+
267
+ # Check at least on domain is given.
268
+ # @return [void]
269
+ # @raise [Error] no domain given
270
+ def check_domains
271
+ if @options[:domains].empty?
272
+ raise Error, 'At leat one domain must be given with --domain ' \
273
+ "option.\nTry 'letscert --help' for more information."
274
+ end
275
+ end
276
+
277
+ # Revoke a certificate
278
+ # @return [Integer] exit status
279
+ def revoke
280
+ data = load_data_from_disk(IOPlugin.registered.keys)
281
+ certificate = Certificate.new(data[:cert])
282
+ if certificate.revoke(data[:account_key], @options)
283
+ RETURN_OK
284
+ else
285
+ RETURN_ERROR
286
+ end
287
+ end
288
+
289
+ # Create/update a certificate
290
+ # @return [Integer] exit status
291
+ # rubocop:disable Style/AccessorMethodName
292
+ def get_certificate
293
+ data = load_data_from_disk(@options[:files])
294
+
295
+ certificate = Certificate.new(data[:cert])
296
+ min_time = @options[:valid_min].to_seconds
297
+ if certificate.valid?(@options[:domains], min_time)
298
+ @logger.info { 'no need to update cert' }
299
+ RETURN_OK
300
+ else
301
+ # update/create cert
302
+ certificate.get data[:account_key], data[:key], @options
303
+ RETURN_OK_CERT
304
+ end
305
+ end
306
+
346
307
  # Load existing data from disk
347
308
  # @param [Array<String>] files
348
309
  # @return [Hash]
@@ -358,9 +319,9 @@ module LetsCert
358
319
  end
359
320
  raise Error unless test
360
321
 
361
- # Merge data into all_data. New value replace old one only if old one was
362
- # not defined
363
- all_data.merge!(data) do |key, oldval, newval|
322
+ # Merge data into all_data. New value replace old one only if old
323
+ # one was not defined
324
+ all_data.merge!(data) do |_key, oldval, newval|
364
325
  oldval || newval
365
326
  end
366
327
  end
@@ -387,6 +348,18 @@ module LetsCert
387
348
  @options[:roots] = roots
388
349
  end
389
350
 
351
+ def persisted_data
352
+ persisted = IOPlugin.empty_data
353
+ @options[:files].each do |file|
354
+ ioplugin = IOPlugin.registered[file]
355
+ next if ioplugin.nil?
356
+ persisted.merge!(ioplugin.persisted) do |_k, oldv, newv|
357
+ oldv || newv
358
+ end
359
+ end
360
+ persisted
361
+ end
362
+
390
363
  end
391
364
 
392
365
  end