letscert 0.3.0 → 0.3.1

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
  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