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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.rubocop.yml +33 -0
- data/Gemfile +2 -0
- data/README.md +6 -3
- data/Rakefile +4 -1
- data/letscert.gemspec +5 -5
- data/lib/letscert/certificate.rb +104 -82
- data/lib/letscert/io_plugin.rb +32 -350
- data/lib/letscert/io_plugins/account_key.rb +29 -0
- data/lib/letscert/io_plugins/cert_file.rb +29 -0
- data/lib/letscert/io_plugins/chain_file.rb +32 -0
- data/lib/letscert/io_plugins/file_io_plugin_mixin.rb +48 -0
- data/lib/letscert/io_plugins/full_chain_file.rb +39 -0
- data/lib/letscert/io_plugins/jwk_io_plugin_mixin.rb +68 -0
- data/lib/letscert/io_plugins/key_file.rb +29 -0
- data/lib/letscert/io_plugins/openssl_io_plugin.rb +68 -0
- data/lib/letscert/loggable.rb +2 -3
- data/lib/letscert/runner.rb +130 -157
- data/lib/letscert/runner/logger_formatter.rb +34 -0
- data/lib/letscert/runner/valid_time.rb +48 -0
- data/lib/letscert/version.rb +1 -1
- metadata +13 -16
- metadata.gz.sig +1 -2
@@ -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
|
data/lib/letscert/loggable.rb
CHANGED
@@ -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
|
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
|
data/lib/letscert/runner.rb
CHANGED
@@ -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
|
-
|
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: '
|
138
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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')
|
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(
|
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,
|
161
|
+
opts.on('--valid-min TIME', ValidTime,
|
162
|
+
'Renew existing certificate if validity',
|
273
163
|
'is lesser than TIME',
|
274
|
-
"(default: #{@options[:valid_min]
|
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(
|
284
|
-
|
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
|
-
|
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 =
|
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
|
362
|
-
# not defined
|
363
|
-
all_data.merge!(data) do |
|
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
|