letscert 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4d86e5665c2e35534ded942c0603cb47b927e05e
4
- data.tar.gz: f2d9fa631de0f0aee3ec3e051f6c2a78ff85bdf1
3
+ metadata.gz: ecc722b11b425c139e92b66d7436be0b818d0d03
4
+ data.tar.gz: 10b112ecdefce5e90e0c5cf8f99cd8bef961c2c0
5
5
  SHA512:
6
- metadata.gz: 103bc74e75d1a69f004951cbd681768a3efd11520c59ed38cc389a81aaf750ec097c94d84a57a9898fc298237309bc84f48d27dc5dd18d371d952ba48eab09e1
7
- data.tar.gz: 717dfaff074bd6c3809045e56ff51e7518c2c0f2bc2ce5990e8777121b3ea6dda3f67e4b90dec891534dd0f9e4cba60a3f72bd36e2e26e62b18de77ee23b89e3
6
+ metadata.gz: 32d5b6a6cfe0fd937506fc60791c793cac8024ab13a04531fc4a4bc56529074131d27f80c642f2d9e68a6026f3081b3d5578c0c2d1343f8a07e7b5acc1eb484c
7
+ data.tar.gz: a2c004cd721d85ce5aec4afe22325eaf9cfd5f70fd173f029bd443826e7cf1361d341ef9db105a69afb91cf6ceca8da37539bdb32e382a629865612101fccbaf
data/README.md CHANGED
@@ -11,11 +11,14 @@ in Ruby.
11
11
 
12
12
  ## Generate a key pair and get signed certificate:
13
13
  With full chain support (`fullchain.pem` file will contain all certificates):
14
+
14
15
  ```bash
15
16
  letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld -f account_key.json -f key.pem -f fullchain.pem
16
17
  ```
18
+
17
19
  else (certificate for example.com is in `cert.pem` file, rest of certification chain
18
20
  is in `chain.pem`):
21
+
19
22
  ```bash
20
23
  letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld -f account_key.json -f key.pem -f cert.pem -f chain.pem
21
24
  ```
@@ -25,14 +28,27 @@ Commands are the sames for certificate renewal.
25
28
 
26
29
  ## Generate a key pair and get a signed certificate for multi-domains:
27
30
  Generate a single certificate for `example.com` and `www.example.com`:
31
+
28
32
  ```bash
29
33
  letscert -d example.com -d www.example.com --default-root /var/www/html --email my.name@domain.tld -f account_key.json -f key.pem -f fullchain.pem
30
34
  ```
31
35
 
32
36
  Command is the same for certificate renewal.
33
37
 
38
+ ## Generate a key pair and get a signed certificate if existing one is valid for less than xx days
39
+
40
+ In this example, `xx` is 10:
41
+
42
+ ```bash
43
+ letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld -f account_key.json -f key.pem -f cert.pem -f chain.pem --valid-min 10d
44
+ ```
45
+
46
+ Valid time may also be set as number of hours (`h` suffix), minutes (`m` suffix) or
47
+ seconds (no suffix).
48
+
34
49
  ## Revoke a key pair:
35
50
  From directory where are stored `account_key.json` and `cert.pem` or `fullchain.pem`:
51
+
36
52
  ```bash
37
53
  letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld --revoke
38
54
  ```
@@ -19,6 +19,7 @@
19
19
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
+ require 'acme-client'
22
23
  require_relative 'loggable'
23
24
 
24
25
  module LetsCert
@@ -28,6 +29,11 @@ module LetsCert
28
29
  class Certificate
29
30
  include Loggable
30
31
 
32
+ # @return [OpenSSL::X509::Certificate,nil]
33
+ attr_reader :cert
34
+ # @return [Acme::Client,nil]
35
+ attr_reader :client
36
+
31
37
 
32
38
  # @param [OpenSSL::X509::Certificate,nil] cert
33
39
  def initialize(cert)
@@ -35,17 +41,29 @@ module LetsCert
35
41
  end
36
42
 
37
43
  # Get a new certificate, or renew an existing one
38
- # @param [OpenSSL::PKey::PKey] account_key private key to authenticate to ACME server
39
- # @param [OpenSSL::PKey::PKey] key private key from which make a certificate
40
- # @param [Hash] data
44
+ # @param [OpenSSL::PKey::PKey,nil] account_key private key to authenticate to ACME
45
+ # server
46
+ # @param [OpenSSL::PKey::PKey, nil] key private key from which make a certificate.
47
+ # If +nil+, generate a new one with +options[:cet_key_size]+ bits.
48
+ # @param [Hash] options option hash
49
+ # @option options [Fixnum] :account_key_size ACME account private key size in bits
50
+ # @option options [Fixnum] :cert_key_size private key size used to generate
51
+ # a certificate
52
+ # @option options [String] :email e-mail used as ACME account
53
+ # @option options [Array<String>] :files plugin names to use
54
+ # @option options [Boolean] :reuse_key reuse private key when getting a new
55
+ # certificate
56
+ # @option options [Hash] :roots hash associating domains as keys to web roots as
57
+ # values
58
+ # @option options [String] :server ACME servel URL
41
59
  def get(account_key, key, options)
42
60
  logger.info {"create key/cert/chain..." }
43
- roots = compute_roots(options)
44
- logger.debug { "webroots are: #{roots.inspect}" }
61
+ check_roots(options[:roots])
62
+ logger.debug { "webroots are: #{options[:roots].inspect}" }
45
63
 
46
64
  client = get_acme_client(account_key, options)
47
65
 
48
- do_challenges client, roots
66
+ do_challenges client, options[:roots]
49
67
 
50
68
  if options[:reuse_key] and !key.nil?
51
69
  logger.info { 'Reuse existing private key' }
@@ -54,7 +72,7 @@ module LetsCert
54
72
  key = OpenSSL::PKey::RSA.generate(options[:cert_key_size])
55
73
  end
56
74
 
57
- csr = Acme::Client::CertificateRequest.new(names: roots.keys,
75
+ csr = Acme::Client::CertificateRequest.new(names: options[:roots].keys,
58
76
  private_key: key)
59
77
  cert = client.new_certificate(csr)
60
78
 
@@ -120,30 +138,17 @@ module LetsCert
120
138
 
121
139
  private
122
140
 
123
- # Compute webroots
124
- # @return [Hash] whre key are domains and value are their webroot path
125
- def compute_roots(options)
126
- roots = {}
127
- no_roots = []
128
-
129
- options[:domains].each do |domain|
130
- match = domain.match(/([\w+\.]+):(.*)/)
131
- if match
132
- roots[match[1]] = match[2]
133
- elsif options[:default_root]
134
- roots[domain] = options[:default_root]
135
- else
136
- no_roots << domain
137
- end
138
- end
141
+ # check webroots.
142
+ # @param [Hash] roots
143
+ # @raise [Error] if some domains have no defined root.
144
+ def check_roots(roots)
145
+ no_roots = roots.select { |k,v| v.nil? }
139
146
 
140
147
  if !no_roots.empty?
141
148
  raise Error, 'root for the following domain(s) are not specified: ' +
142
- no_roots.join(', ') + ".\nTry --default_root or use " +
149
+ no_roots.keys.join(', ') + ".\nTry --default_root or use " +
143
150
  '-d example.com:/var/www/html syntax.'
144
151
  end
145
-
146
- roots
147
152
  end
148
153
 
149
154
  # Get ACME client.
@@ -33,10 +33,6 @@ module LetsCert
33
33
  # @return [String]
34
34
  attr_reader :name
35
35
 
36
- # Allowed plugin names
37
- ALLOWED_PLUGINS = %w(account_key.json cert.der cert.pem chain.pem full.pem) +
38
- %w(fullchain.pem key.der key.pem)
39
-
40
36
 
41
37
  # Registered plugins
42
38
  @@registered = {}
@@ -31,6 +31,9 @@ module LetsCert
31
31
  # Runner class: analyse and execute CLI commands.
32
32
  # @author Sylvain Daubert
33
33
  class Runner
34
+ # Get options
35
+ # @return [Hash]
36
+ attr_reader :options
34
37
 
35
38
  # Custom logger formatter
36
39
  class LoggerFormatter < Logger::Formatter
@@ -59,6 +62,47 @@ module LetsCert
59
62
 
60
63
  end
61
64
 
65
+ # Class used to process validation time from String.
66
+ # @author Sylvain Daubert
67
+ class ValidTime
68
+
69
+ # @param [String] str time string. May be:
70
+ # * an integer -> time in seconds
71
+ # * an integer plus a letter:
72
+ # * 30m: 30 minutes,
73
+ # * 30h: 30 hours,
74
+ # * 30d: 30 days.
75
+ def initialize(str)
76
+ m = str.match(/^(\d+)([mhd])?$/)
77
+ if m
78
+ case m[2]
79
+ when nil
80
+ @seconds = m[1].to_i
81
+ when 'm'
82
+ @seconds = m[1].to_i * 60
83
+ when 'h'
84
+ @seconds = m[1].to_i * 60 * 60
85
+ when 'd'
86
+ @seconds = m[1].to_i * 24 * 60 * 60
87
+ end
88
+ else
89
+ raise OptionParser::InvalidArgument, "invalid argument: --valid-min #{str}"
90
+ end
91
+ @string = str
92
+ end
93
+
94
+ # Get time in seconds
95
+ # @return [Integer]
96
+ def to_seconds
97
+ @seconds
98
+ end
99
+
100
+ # Get time as string
101
+ # @return [String]
102
+ def to_s
103
+ @string
104
+ end
105
+ end
62
106
 
63
107
  # Exit value for OK
64
108
  RETURN_OK = 1
@@ -86,11 +130,10 @@ module LetsCert
86
130
  domains: [],
87
131
  files: [],
88
132
  cert_key_size: 2048,
89
- valid_min: 30 * 24 * 60 * 60,
90
- account_key_public_exponent: 65537,
133
+ valid_min: ValidTime.new('30d'),
91
134
  account_key_size: 4096,
92
135
  tos_sha256: '33d233c8ab558ba6c8ebc370a509acdded8b80e5d587aa5d192193f35226540f',
93
- user_agent: 'letscert/0',
136
+ user_agent: "letscert/#{VERSION.gsub(/\..*/, '')}",
94
137
  server: 'https://acme-v01.api.letsencrypt.org/directory',
95
138
  }
96
139
 
@@ -144,23 +187,12 @@ module LetsCert
144
187
  RETURN_ERROR
145
188
  end
146
189
  else
147
- # Check all components are covered by plugins
148
- persisted = IOPlugin.empty_data
149
- @options[:files].each do |file|
150
- persisted.merge!(IOPlugin.registered[file].persisted) do |k, oldv, newv|
151
- oldv || newv
152
- end
153
- end
154
- not_persisted = persisted.keys.find_all { |k| !persisted[k] }
155
- unless not_persisted.empty?
156
- raise Error, 'Selected IO plugins do not cover following components: ' +
157
- not_persisted.join(', ')
158
- end
190
+ check_persisted
159
191
 
160
192
  data = load_data_from_disk(@options[:files])
161
193
 
162
194
  certificate = Certificate.new(data[:cert])
163
- if certificate.valid?(@options[:domains], @options[:valid_min])
195
+ if certificate.valid?(@options[:domains], @options[:valid_min].to_seconds)
164
196
  @logger.info { 'no need to update cert' }
165
197
  RETURN_OK
166
198
  else
@@ -226,13 +258,17 @@ module LetsCert
226
258
 
227
259
  opts.on('--cert-key-size BITS', Integer,
228
260
  'Certificate key size in bits',
229
- '(default: 2048)') do |bits|
261
+ "(default: #{@options[:cert_key_size]})") do |bits|
230
262
  @options[:cert_key_size] = bits
231
263
  end
232
264
 
233
- opts.on('--valid-min SECONDS', Integer, 'Renew existing certificate if validity',
234
- 'is lesser than SECONDS (default: 2592000 (30 days))') do |time|
235
- @options[:valid_min] = time
265
+ opts.accept(ValidTime) do |valid_time|
266
+ ValidTime.new(valid_time)
267
+ end
268
+ opts.on('--valid-min TIME', ValidTime, 'Renew existing certificate if validity',
269
+ 'is lesser than TIME',
270
+ "(default: #{@options[:valid_min].to_s})") do |vt|
271
+ @options[:valid_min] = vt
236
272
  end
237
273
 
238
274
  opts.on('--reuse-key', 'Reuse previous private key') do |rk|
@@ -244,18 +280,14 @@ module LetsCert
244
280
  " by --server")
245
281
  opts.separator('')
246
282
 
247
- opts.on('--account-key-public-exponent BITS', Integer,
248
- 'Account key public exponent value (default: 65537)') do |bits|
249
- @options[:account_key_public_exponent] = bits
250
- end
251
-
252
283
  opts.on('--account-key-size BITS', Integer,
253
- 'Account key size (default: 4096)') do |bits|
284
+ "Account key size (default: #{@options[:account_key_size]})") do |bits|
254
285
  @options[:account_key_size] = bits
255
286
  end
256
287
 
257
288
  opts.on('--tos-sha256 HASH', String,
258
- 'SHA-256 digest of the content of Terms Of Service URI') do |hash|
289
+ 'SHA-256 digest of the content of Terms',
290
+ 'Of Service URI') do |hash|
259
291
  @options[:tos_sha256] = hash
260
292
  end
261
293
 
@@ -272,17 +304,36 @@ module LetsCert
272
304
  opts.separator('')
273
305
 
274
306
  opts.on('--user-agent NAME', 'User-Agent sent in all HTTP requests',
275
- '(default: letscert/0)') do |ua|
307
+ "(default: #{@options[:user_agent]})") do |ua|
276
308
  @options[:user_agent] = ua
277
309
  end
278
310
 
279
311
  opts.on('--server URI', 'URI for the CA ACME API endpoint',
280
- '(default: https://acme-v01.api.letsencrypt.org/directory)') do |uri|
312
+ "(default: #{@options[:server]})") do |uri|
281
313
  @options[:server] = uri
282
314
  end
283
315
  end
284
316
 
285
317
  @opt_parser.parse!
318
+ compute_roots
319
+ end
320
+
321
+ # Check all components are covered by plugins
322
+ # @raise [Error]
323
+ def check_persisted
324
+ persisted = IOPlugin.empty_data
325
+
326
+ @options[:files].each do |file|
327
+ persisted.merge!(IOPlugin.registered[file].persisted) do |k, oldv, newv|
328
+ oldv || newv
329
+ end
330
+ end
331
+ not_persisted = persisted.keys.find_all { |k| !persisted[k] }
332
+
333
+ unless not_persisted.empty?
334
+ raise Error, 'Selected IO plugins do not cover following components: ' +
335
+ not_persisted.join(', ')
336
+ end
286
337
  end
287
338
 
288
339
 
@@ -313,6 +364,25 @@ module LetsCert
313
364
  all_data
314
365
  end
315
366
 
367
+ # Compute webroots and set +@options[:roots]+
368
+ # @return [Hash] where keys are domains and value are their webroot path
369
+ def compute_roots
370
+ roots = {}
371
+
372
+ @options[:domains].each do |domain|
373
+ match = domain.match(/([-\w\.]+):(.*)/)
374
+ if match
375
+ roots[match[1]] = match[2]
376
+ elsif @options[:default_root]
377
+ roots[domain] = @options[:default_root]
378
+ else
379
+ roots[domain] = nil
380
+ end
381
+ end
382
+
383
+ @options[:roots] = roots
384
+ end
385
+
316
386
  end
317
387
 
318
388
  end
data/lib/letscert.rb CHANGED
@@ -24,7 +24,7 @@
24
24
  module LetsCert
25
25
 
26
26
  # Letscert version number
27
- VERSION = '0.3.0'
27
+ VERSION = '0.3.1'
28
28
 
29
29
 
30
30
  # Base error class
@@ -6,33 +6,120 @@ module LetsCert
6
6
 
7
7
  before(:all) { Certificate.logger = Logger.new('/dev/null') }
8
8
 
9
- context '#valid?' do
9
+ before(:all) do
10
+ root_key = OpenSSL::PKey::RSA.new(512)
11
+
12
+ @domains = %w(example.org www.example.org)
13
+
14
+ key = OpenSSL::PKey::RSA.new(512)
15
+ @cert = OpenSSL::X509::Certificate.new
16
+ @cert.version = 2
17
+ @cert.serial = 2
18
+ @cert.issuer = OpenSSL::X509::Name.parse "/DC=letscert/CN=CA"
19
+ @cert.public_key = key.public_key
20
+ @cert.not_before = Time.now
21
+ # 20 days validity
22
+ @cert.not_after = @cert.not_before + 20 * 24 * 60 * 60
23
+ ef = OpenSSL::X509::ExtensionFactory.new
24
+ ef.subject_certificate = @cert
25
+ @domains.each do |domain|
26
+ @cert.add_extension(ef.create_extension('subjectAltName',
27
+ "DNS:#{domain}",
28
+ false))
29
+ end
30
+ @cert.sign(root_key, OpenSSL::Digest::SHA256.new)
31
+
32
+ # minimum size accepted by ACME server
33
+ @account_key2048 = OpenSSL::PKey::RSA.new(2048)
34
+ end
35
+
36
+ let(:certificate) { Certificate.new(@cert) }
37
+
38
+ context '#get' do
39
+
40
+ it 'checks all domains have a root' do
41
+ runner = Runner.new
42
+ ARGV.clear
43
+
44
+ ARGV << '-d' << 'example.com:/var/ww/html'
45
+ ARGV << '--server' << 'https://acme-staging.api.letsencrypt.org/directory'
46
+ runner.parse_options
47
+ # raise error because no e-mail address was given
48
+ expect { certificate.get(nil, nil, runner.options) }.
49
+ to raise_error(Acme::Client::Error)
50
+
51
+ ARGV.clear
52
+ ARGV << '-d' << 'example.com:/var/www/html'
53
+ ARGV << '-d' << 'www.example.com'
54
+ ARGV << '--server' << 'https://acme-staging.api.letsencrypt.org/directory'
55
+ runner.options[:domains] = []
56
+ runner.parse_options
57
+ expect { certificate.get(nil, nil, runner.options) }.
58
+ to raise_error(LetsCert::Error).
59
+ with_message(/not specified: www\.example\.com\./)
60
+
61
+ ARGV.clear
62
+ ARGV << '-d' << 'example.com:/var/www/html'
63
+ ARGV << '-d' << 'www.example.com'
64
+ ARGV << '--default-root' << '/opt/www'
65
+ ARGV << '--server' << 'https://acme-staging.api.letsencrypt.org/directory'
66
+ runner.parse_options
67
+ # raise error because no e-mail address was given
68
+ expect { certificate.get(nil, nil, runner.options) }.
69
+ to raise_error(Acme::Client::Error)
70
+ expect(runner.options[:roots]['example.com']).to eq('/var/www/html')
71
+ expect(runner.options[:roots]['www.example.com']).to eq('/opt/www')
72
+ end
73
+
74
+ it 'uses existing account key' do
75
+ options = { roots: { 'example.com' => '/var/www/html' } }
10
76
 
11
- before(:all) do
12
- root_key = OpenSSL::PKey::RSA.new(512)
13
-
14
- @domains = %w(example.org www.example.org)
15
-
16
- key = OpenSSL::PKey::RSA.new(512)
17
- @cert = OpenSSL::X509::Certificate.new
18
- @cert.version = 2
19
- @cert.serial = 2
20
- @cert.issuer = OpenSSL::X509::Name.parse "/DC=letscert/CN=CA"
21
- @cert.public_key = key.public_key
22
- @cert.not_before = Time.now
23
- # 20 days validity
24
- @cert.not_after = @cert.not_before + 20 * 24 * 60 * 60
25
- ef = OpenSSL::X509::ExtensionFactory.new
26
- ef.subject_certificate = @cert
27
- @domains.each do |domain|
28
- @cert.add_extension(ef.create_extension('subjectAltName',
29
- "DNS:#{domain}",
30
- false))
31
- end
32
- @cert.sign(root_key, OpenSSL::Digest::SHA256.new)
33
- end
34
-
35
- let(:certificate) { Certificate.new(@cert) }
77
+ # Connection error: no server to connect to
78
+ expect { certificate.get(@account_key2048, nil, options) }.
79
+ to raise_error(Faraday::ConnectionFailed)
80
+ expect(certificate.client.private_key).to eq(@account_key2048)
81
+ end
82
+
83
+ it 'creates an ACME account key if non exists' do
84
+ options = {
85
+ roots: { 'example.com' => '/var/www/html' },
86
+ account_key_size: 128,
87
+ }
88
+
89
+ # Connection error: no server to connect to
90
+ expect { certificate.get(nil, nil, options) }.
91
+ to raise_error(Faraday::ConnectionFailed)
92
+ expect(certificate.client.private_key).to be_a(OpenSSL::PKey::RSA)
93
+ end
94
+
95
+ it 'creates an ACME client with provided account key and end point' do
96
+ options = {
97
+ roots: { 'example.com' => '/var/www/html' },
98
+ server: 'https://acme-staging.api.letsencrypt.org/directory',
99
+ }
100
+
101
+ # Acme error: not valid e-mail address
102
+ expect { certificate.get(@account_key2048, nil, options) }.
103
+ to raise_error(Acme::Client::Error)
104
+ expect(certificate.client.private_key).to eq(@account_key2048)
105
+ expect(certificate.client.instance_eval { @endpoint }).to eq(options[:server])
106
+ end
107
+
108
+ it 'raises when register without e-mail' do
109
+ options = {
110
+ roots: { 'example.com' => '/var/www/html' },
111
+ server: 'https://acme-staging.api.letsencrypt.org/directory',
112
+ }
113
+
114
+ # Acme error: not valid e-mail address
115
+ expect { certificate.get(@account_key2048, nil, options) }.
116
+ to raise_error(Acme::Client::Error).
117
+ with_message('not a valid e-mail address')
118
+ end
119
+
120
+ end
121
+
122
+ context '#valid?' do
36
123
 
37
124
  it 'checks whether a certificate is valid given a minimum valid duration' do
38
125
  expect(certificate.valid?(@domains)).to be(true)
@@ -0,0 +1,127 @@
1
+ require_relative 'spec_helper'
2
+
3
+ module LetsCert
4
+
5
+ describe Runner do
6
+
7
+ before(:each) { ARGV.clear }
8
+
9
+ let(:runner) { Runner.new }
10
+
11
+ context '#parse_options' do
12
+
13
+ it 'accepts --domain with DOMAIN only' do
14
+ ARGV << '--domain' << 'example.com'
15
+
16
+ runner.parse_options
17
+ expect(runner.options[:domains]).to be_a(Array)
18
+ expect(runner.options[:domains].size).to eq(1)
19
+ expect(runner.options[:domains]).to include('example.com')
20
+ end
21
+
22
+ it 'accepts --domain with DOMAIN:PATH' do
23
+ ARGV << '--domain' << 'example.com:/var/www/html'
24
+
25
+ runner.parse_options
26
+ expect(runner.options[:domains]).to be_a(Array)
27
+ expect(runner.options[:domains].size).to eq(1)
28
+ expect(runner.options[:domains]).to include('example.com:/var/www/html')
29
+ end
30
+
31
+ it 'accepts multiple domains with --domain option' do
32
+ ARGV << '--domain' << 'example.com'
33
+ ARGV << '--domain' << 'www.example.com'
34
+ ARGV << '--domain' << 'www2.example.com'
35
+
36
+ runner.parse_options
37
+ expect(runner.options[:domains]).to be_a(Array)
38
+ expect(runner.options[:domains].size).to eq(3)
39
+ expect(runner.options[:domains]).to include('example.com')
40
+ expect(runner.options[:domains]).to include('www.example.com')
41
+ expect(runner.options[:domains]).to include('www2.example.com')
42
+ end
43
+
44
+ it 'sets default root path with --default-root for domains without PATH' do
45
+ ARGV << '--domain' << 'example.com'
46
+ ARGV << '--domain' << 'another-example.com:/var/www/html'
47
+ ARGV << '--default-root' << '/opt/www'
48
+
49
+ runner.parse_options
50
+ expect(runner.options[:default_root]).to eq('/opt/www')
51
+ expect(runner.options[:roots]).to be_a(Hash)
52
+ expect(runner.options[:roots]['example.com']).to eq(runner.options[:default_root])
53
+ expect(runner.options[:roots]['another-example.com']).to eq('/var/www/html')
54
+ end
55
+
56
+ it 'accepts multiples files with --file option' do
57
+ ARGV << '--file' << 'key.pem'
58
+ ARGV << '-f' << 'cert.pem'
59
+
60
+ runner.parse_options
61
+ expect(runner.options[:files]).to be_a(Array)
62
+ expect(runner.options[:files].size).to eq(2)
63
+ expect(runner.options[:files]).to include('key.pem')
64
+ expect(runner.options[:files]).to include('cert.pem')
65
+ end
66
+
67
+ it 'sets minimum validity time with --valid-min option' do
68
+ ARGV << '--valid-min' << '30000'
69
+
70
+ runner.parse_options
71
+ expect(runner.options[:valid_min].to_seconds).to eq(30000)
72
+ end
73
+
74
+ it '--valid-min option accepts minute format' do
75
+ minutes = 156
76
+ ARGV << '--valid-min' << "#{minutes}m"
77
+
78
+ runner.parse_options
79
+ expect(runner.options[:valid_min].to_seconds).to eq(minutes * 60)
80
+ end
81
+
82
+ it '--valid-min option accepts hour format' do
83
+ hours = 4
84
+ ARGV << '--valid-min' << "#{hours}h"
85
+
86
+ runner.parse_options
87
+ expect(runner.options[:valid_min].to_seconds).to eq(hours * 3600)
88
+ end
89
+
90
+ it '--valid-min option accepts day format' do
91
+ days = 20
92
+ ARGV << '--valid-min' << "#{days}d"
93
+
94
+ runner.parse_options
95
+ expect(runner.options[:valid_min].to_seconds).to eq(days * 24 * 3600)
96
+ end
97
+
98
+ end
99
+
100
+ it '#check_persisted checks all mandatory components are covered by files' do
101
+ expect { runner.check_persisted }.to raise_error(LetsCert::Error)
102
+
103
+ all_needed = [%w(account_key.json cert.pem chain.pem key.pem),
104
+ %w(account_key.json cert.der chain.pem key.der),
105
+ %w(account_key.json fullchain.pem key.pem),
106
+ %w(account_key.json fullchain.pem key.der)]
107
+ all_needed.each do |needed|
108
+ needed.size.times do |nb|
109
+ ARGV.clear
110
+ runner.options[:files] = []
111
+ 0.upto(nb) do |i|
112
+ ARGV << '-f' << needed[i]
113
+ end
114
+ runner.parse_options
115
+
116
+ if nb == needed.size - 1
117
+ expect { runner.check_persisted }.to_not raise_error
118
+ else
119
+ expect { runner.check_persisted }.to raise_error(LetsCert::Error)
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ end
126
+
127
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'spec_helper'
2
+
3
+ module LetsCert
4
+
5
+ describe Runner do
6
+ end
7
+
8
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: letscert
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sylvain Daubert
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-16 00:00:00.000000000 Z
11
+ date: 2016-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: acme-client
@@ -95,6 +95,8 @@ files:
95
95
  - spec/key.der
96
96
  - spec/key.pem
97
97
  - spec/loggable_spec.rb
98
+ - spec/runner_spec.rb
99
+ - spec/runner_spec.rb~
98
100
  - spec/spec_helper.rb
99
101
  - spec/test.json
100
102
  - tasks/gem.rake