acmesmith 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.dockerignore +6 -0
- data/.github/workflows/build.yml +123 -0
- data/.gitignore +0 -1
- data/CHANGELOG.md +35 -0
- data/Dockerfile +29 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +73 -0
- data/LICENSE.txt +1 -1
- data/README.md +71 -93
- data/acmesmith.gemspec +1 -1
- data/config.sample.yml +41 -2
- data/docs/challenge_responders/route53.md +28 -0
- data/docs/examples/UpdateWindowsCertificate.ps1 +58 -0
- data/docs/post_issuing_hooks/acm.md +16 -0
- data/docs/post_issuing_hooks/shell.md +17 -0
- data/docs/storages/filesystem.md +11 -0
- data/docs/storages/s3.md +32 -0
- data/lib/acmesmith/account_key.rb +12 -1
- data/lib/acmesmith/authorization_service.rb +175 -0
- data/lib/acmesmith/certificate.rb +42 -11
- data/lib/acmesmith/challenge_responder_filter.rb +23 -0
- data/lib/acmesmith/challenge_responders/base.rb +11 -2
- data/lib/acmesmith/challenge_responders/pebble_challtestsrv_dns.rb +53 -0
- data/lib/acmesmith/challenge_responders/route53.rb +13 -2
- data/lib/acmesmith/client.rb +13 -131
- data/lib/acmesmith/config.rb +23 -2
- data/lib/acmesmith/ordering_service.rb +104 -0
- data/lib/acmesmith/storages/base.rb +15 -0
- data/lib/acmesmith/storages/s3.rb +3 -3
- data/lib/acmesmith/version.rb +1 -1
- metadata +19 -6
@@ -2,17 +2,32 @@ require 'openssl'
|
|
2
2
|
|
3
3
|
module Acmesmith
|
4
4
|
class Certificate
|
5
|
+
class PrivateKeyDecrypted < StandardError; end
|
5
6
|
class PassphraseRequired < StandardError; end
|
6
7
|
|
8
|
+
CertificateExport = Struct.new(:certificate, :chain, :fullchain, :private_key, keyword_init: true)
|
9
|
+
|
10
|
+
# Split string containing multiple PEMs into Array of PEM strings.
|
11
|
+
# @param [String]
|
12
|
+
# @return [Array<String>]
|
7
13
|
def self.split_pems(pems)
|
8
14
|
pems.each_line.slice_before(/^-----BEGIN CERTIFICATE-----$/).map(&:join)
|
9
15
|
end
|
10
16
|
|
17
|
+
# Return Acmesmith::Certificate by an issued certificate
|
18
|
+
# @param pem_chain [String]
|
19
|
+
# @param csr [Acme::Client::CertificateRequest]
|
20
|
+
# @return [Acmesmith::Certificate]
|
11
21
|
def self.by_issuance(pem_chain, csr)
|
12
22
|
pems = split_pems(pem_chain)
|
13
|
-
new(
|
23
|
+
new(pems[0], pems[1..-1], csr.private_key, nil, csr)
|
14
24
|
end
|
15
25
|
|
26
|
+
# @param certificate [OpenSSL::X509::Certificate, String]
|
27
|
+
# @param chain [String, Array<String>, Array<OpenSSL::X509::Certificate>]
|
28
|
+
# @param private_key [String, OpenSSL::PKey::RSA]
|
29
|
+
# @param key_passphrase [String, nil]
|
30
|
+
# @param csr [String, OpenSSL::X509::Request, nil]
|
16
31
|
def initialize(certificate, chain, private_key, key_passphrase = nil, csr = nil)
|
17
32
|
@certificate = case certificate
|
18
33
|
when OpenSSL::X509::Certificate
|
@@ -48,7 +63,7 @@ module Acmesmith
|
|
48
63
|
when String
|
49
64
|
@raw_private_key = private_key
|
50
65
|
if key_passphrase
|
51
|
-
self.key_passphrase = key_passphrase
|
66
|
+
self.key_passphrase = key_passphrase
|
52
67
|
else
|
53
68
|
begin
|
54
69
|
@private_key = OpenSSL::PKey::RSA.new(@raw_private_key) { nil }
|
@@ -72,10 +87,18 @@ module Acmesmith
|
|
72
87
|
end
|
73
88
|
end
|
74
89
|
|
75
|
-
|
90
|
+
# @return [OpenSSL::X509::Certificate]
|
91
|
+
attr_reader :certificate
|
92
|
+
# @return [Array<OpenSSL::X509::Certificate>]
|
93
|
+
attr_reader :chain
|
94
|
+
# @return [OpenSSL::X509::Request]
|
95
|
+
attr_reader :csr
|
76
96
|
|
97
|
+
# Try to decrypt private_key if encrypted.
|
98
|
+
# @param pw [String] passphrase for encrypted PEM
|
99
|
+
# @raise [PrivateKeyDecrypted] if private_key is decrypted
|
77
100
|
def key_passphrase=(pw)
|
78
|
-
raise 'private_key already given' if @private_key
|
101
|
+
raise PrivateKeyDecrypted, 'private_key already given' if @private_key
|
79
102
|
|
80
103
|
@private_key = OpenSSL::PKey::RSA.new(@raw_private_key, pw)
|
81
104
|
|
@@ -83,44 +106,52 @@ module Acmesmith
|
|
83
106
|
nil
|
84
107
|
end
|
85
108
|
|
109
|
+
# @return [OpenSSL::PKey::RSA]
|
110
|
+
# @raise [PassphraseRequired] if private_key is not yet decrypted
|
86
111
|
def private_key
|
87
112
|
return @private_key if @private_key
|
88
113
|
raise PassphraseRequired, 'key_passphrase required'
|
89
114
|
end
|
90
115
|
|
116
|
+
# @return [String] leaf certificate + full certificate chain
|
91
117
|
def fullchain
|
92
118
|
"#{certificate.to_pem}\n#{issuer_pems}".gsub(/\n+/,?\n)
|
93
119
|
end
|
94
120
|
|
121
|
+
# @return [String] issuer certificate chain
|
95
122
|
def issuer_pems
|
96
123
|
chain.map(&:to_pem).join("\n")
|
97
124
|
end
|
98
125
|
|
126
|
+
# @return [String] common name
|
99
127
|
def common_name
|
100
128
|
certificate.subject.to_a.assoc('CN')[1]
|
101
129
|
end
|
102
130
|
|
131
|
+
# @return [Array<String>] Subject Alternative Names (dNSname)
|
103
132
|
def sans
|
104
133
|
certificate.extensions.select { |_| _.oid == 'subjectAltName' }.flat_map do |ext|
|
105
134
|
ext.value.split(/,\s*/).select { |_| _.start_with?('DNS:') }.map { |_| _[4..-1] }
|
106
135
|
end
|
107
136
|
end
|
108
137
|
|
138
|
+
# @return [String] Version string (consists of NotBefore time & certificate serial)
|
109
139
|
def version
|
110
140
|
"#{certificate.not_before.utc.strftime('%Y%m%d-%H%M%S')}_#{certificate.serial.to_i.to_s(16)}"
|
111
141
|
end
|
112
142
|
|
143
|
+
# @return [OpenSSL::PKCS12]
|
113
144
|
def pkcs12(passphrase)
|
114
|
-
OpenSSL::PKCS12.create(passphrase, common_name, private_key, certificate)
|
145
|
+
OpenSSL::PKCS12.create(passphrase, common_name, private_key, certificate, chain)
|
115
146
|
end
|
116
147
|
|
148
|
+
# @return [CertificateExport]
|
117
149
|
def export(passphrase, cipher: OpenSSL::Cipher.new('aes-256-cbc'))
|
118
|
-
|
119
|
-
h
|
120
|
-
h
|
121
|
-
h
|
122
|
-
|
123
|
-
h[:private_key] = if passphrase
|
150
|
+
CertificateExport.new.tap do |h|
|
151
|
+
h.certificate = certificate.to_pem
|
152
|
+
h.chain = issuer_pems
|
153
|
+
h.fullchain = fullchain
|
154
|
+
h.private_key = if passphrase
|
124
155
|
private_key.export(cipher, passphrase)
|
125
156
|
else
|
126
157
|
private_key.export
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Acmesmith
|
2
|
+
class ChallengeResponderFilter
|
3
|
+
def initialize(responder, subject_name_exact: nil, subject_name_suffix: nil, subject_name_regexp: nil)
|
4
|
+
@responder = responder
|
5
|
+
@subject_name_exact = subject_name_exact && [*subject_name_exact].flatten.compact
|
6
|
+
@subject_name_suffix = subject_name_suffix && [*subject_name_suffix].flatten.compact
|
7
|
+
@subject_name_regexp = subject_name_regexp && [*subject_name_regexp].flatten.compact.map{ |_| Regexp.new(_) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def applicable?(domain)
|
11
|
+
if @subject_name_exact
|
12
|
+
return false unless @subject_name_exact.include?(domain)
|
13
|
+
end
|
14
|
+
if @subject_name_suffix
|
15
|
+
return false unless @subject_name_suffix.any? { |suffix| domain.end_with?(suffix) }
|
16
|
+
end
|
17
|
+
if @subject_name_regexp
|
18
|
+
return false unless @subject_name_regexp.any? { |regexp| domain.match?(regexp) }
|
19
|
+
end
|
20
|
+
@responder.applicable?(domain)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,12 +1,19 @@
|
|
1
1
|
module Acmesmith
|
2
2
|
module ChallengeResponders
|
3
3
|
class Base
|
4
|
-
#
|
4
|
+
# @param type [String] ACME challenge type (dns-01, http-01, ...)
|
5
|
+
# @return [true, false] true when given challenge type is supported
|
5
6
|
def support?(type)
|
6
7
|
raise NotImplementedError
|
7
8
|
end
|
8
9
|
|
9
|
-
#
|
10
|
+
# @param domain [String] target FQDN for a ACME authorization challenge
|
11
|
+
# @return [true, false] true when a responder is able to challenge against given domain name
|
12
|
+
def applicable?(domain)
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [true, false] true when implements #respond_all, #cleanup_all
|
10
17
|
def cap_respond_all?
|
11
18
|
false
|
12
19
|
end
|
@@ -15,6 +22,7 @@ module Acmesmith
|
|
15
22
|
end
|
16
23
|
|
17
24
|
# Respond to the given challenges (1 or more).
|
25
|
+
# @param domain_and_challenges [Array<(String, Acme::Client::Resources::Challenges::Base)>] array of tuple of domain name and ACME challenge
|
18
26
|
def respond_all(*domain_and_challenges)
|
19
27
|
if cap_respond_all?
|
20
28
|
raise NotImplementedError
|
@@ -26,6 +34,7 @@ module Acmesmith
|
|
26
34
|
end
|
27
35
|
|
28
36
|
# Clean up responses for the given challenges (1 or more).
|
37
|
+
# @param domain_and_challenges [Array<(String, Acme::Client::Resources::Challenges::Base)>] array of tuple of domain name and ACME challenge
|
29
38
|
def cleanup_all(*domain_and_challenges)
|
30
39
|
if cap_respond_all?
|
31
40
|
raise NotImplementedError
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'acmesmith/challenge_responders/base'
|
2
|
+
require 'net/http'
|
3
|
+
require 'uri'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Acmesmith
|
7
|
+
module ChallengeResponders
|
8
|
+
class PebbleChalltestsrvDns < Base
|
9
|
+
def support?(type)
|
10
|
+
# Acme::Client::Resources::Challenges::DNS01
|
11
|
+
type == 'dns-01'
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(url: 'http://localhost:8055')
|
15
|
+
warn_test
|
16
|
+
@url = URI.parse(url)
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :url
|
20
|
+
|
21
|
+
def respond(domain, challenge)
|
22
|
+
warn_test
|
23
|
+
|
24
|
+
Net::HTTP.post(
|
25
|
+
URI.join(url,"/set-txt"),
|
26
|
+
{
|
27
|
+
host: "#{challenge.record_name}.#{domain}.",
|
28
|
+
value: challenge.record_content,
|
29
|
+
}.to_json,
|
30
|
+
).value
|
31
|
+
end
|
32
|
+
|
33
|
+
def cleanup(domain, challenge)
|
34
|
+
warn_test
|
35
|
+
|
36
|
+
Net::HTTP.post(
|
37
|
+
URI.join(url,"/clear-txt"),
|
38
|
+
{
|
39
|
+
host: "#{challenge.record_name}.#{domain}.",
|
40
|
+
}.to_json,
|
41
|
+
).value
|
42
|
+
end
|
43
|
+
|
44
|
+
def warn_test
|
45
|
+
unless ENV['CI']
|
46
|
+
$stderr.puts '!!!!!!!!! WARNING WARNING WARNING !!!!!!!!!'
|
47
|
+
$stderr.puts '!!!! pebble-challtestsrv command is for TEST USAGE ONLY. It is trivially insecure, offering no authentication. Only use pebble-challtestsrv in a controlled test environment.'
|
48
|
+
$stderr.puts '!!!! https://github.com/letsencrypt/pebble/blob/master/cmd/pebble-challtestsrv/README.md'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -17,10 +17,21 @@ module Acmesmith
|
|
17
17
|
true
|
18
18
|
end
|
19
19
|
|
20
|
-
def initialize(aws_access_key: nil, hosted_zone_map: {})
|
21
|
-
|
20
|
+
def initialize(aws_access_key: nil, assume_role: nil, hosted_zone_map: {})
|
21
|
+
aws_options = {region: 'us-east-1'}.tap do |opt|
|
22
22
|
opt[:credentials] = Aws::Credentials.new(aws_access_key['access_key_id'], aws_access_key['secret_access_key'], aws_access_key['session_token']) if aws_access_key
|
23
|
+
end
|
24
|
+
|
25
|
+
@route53 = Aws::Route53::Client.new(aws_options.dup.tap do |opt|
|
26
|
+
case
|
27
|
+
when assume_role
|
28
|
+
opt[:credentials] = Aws::AssumeRoleCredentials.new(
|
29
|
+
client: Aws::STS::Client.new(aws_options),
|
30
|
+
**({role_session_name: "acmesmith-#{$$}"}.merge(assume_role.map{ |k,v| [k.to_sym,v] }.to_h)),
|
31
|
+
)
|
32
|
+
end
|
23
33
|
end)
|
34
|
+
|
24
35
|
@hosted_zone_map = hosted_zone_map
|
25
36
|
@hosted_zone_cache = {}
|
26
37
|
end
|
data/lib/acmesmith/client.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'acmesmith/account_key'
|
2
2
|
require 'acmesmith/certificate'
|
3
|
+
require 'acmesmith/authorization_service'
|
4
|
+
require 'acmesmith/ordering_service'
|
3
5
|
require 'acmesmith/save_certificate_service'
|
4
6
|
require 'acme-client'
|
5
7
|
|
@@ -11,7 +13,7 @@ module Acmesmith
|
|
11
13
|
|
12
14
|
def new_account(contact, tos_agreed: true)
|
13
15
|
key = AccountKey.generate
|
14
|
-
acme = Acme::Client.new(private_key: key.private_key, directory: config.
|
16
|
+
acme = Acme::Client.new(private_key: key.private_key, directory: config.directory, connection_options: config.connection_options, bad_nonce_retry: config.bad_nonce_retry)
|
15
17
|
acme.new_account(contact: contact, terms_of_service_agreed: tos_agreed)
|
16
18
|
|
17
19
|
storage.put_account_key(key, account_key_passphrase)
|
@@ -20,31 +22,16 @@ module Acmesmith
|
|
20
22
|
end
|
21
23
|
|
22
24
|
def order(*identifiers, not_before: nil, not_after: nil)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
order = acme.new_order(identifiers: identifiers, not_before: not_before, not_after: not_after)
|
33
|
-
|
34
|
-
unless order.authorizations.empty? || order.status == 'ready'
|
35
|
-
puts "=> Looking for required domain authorizations"
|
36
|
-
puts
|
37
|
-
order.authorizations.map(&:domain).each do |domain|
|
38
|
-
puts " * #{domain}"
|
39
|
-
end
|
40
|
-
puts
|
41
|
-
|
42
|
-
process_authorizations(order.authorizations)
|
43
|
-
end
|
44
|
-
|
45
|
-
cert = process_order_finalization(order, csr)
|
25
|
+
order = OrderingService.new(
|
26
|
+
acme: acme,
|
27
|
+
identifiers: identifiers,
|
28
|
+
challenge_responder_rules: config.challenge_responders,
|
29
|
+
not_before: not_before,
|
30
|
+
not_after: not_after
|
31
|
+
)
|
32
|
+
order.perform!
|
33
|
+
cert = order.certificate
|
46
34
|
|
47
|
-
puts "=> Certificate issued"
|
48
35
|
puts
|
49
36
|
print " * securing into the storage ..."
|
50
37
|
storage.put_certificate(cert, certificate_key_passphrase)
|
@@ -175,111 +162,6 @@ module Acmesmith
|
|
175
162
|
|
176
163
|
private
|
177
164
|
|
178
|
-
def process_order_finalization(order, csr)
|
179
|
-
puts "=> Finalizing the order"
|
180
|
-
puts
|
181
|
-
|
182
|
-
print " * Requesting..."
|
183
|
-
order.finalize(csr: csr)
|
184
|
-
puts" [ ok ]"
|
185
|
-
|
186
|
-
while %w(ready processing).include?(order.status)
|
187
|
-
order.reload()
|
188
|
-
puts " * Waiting for procession: status=#{order.status}"
|
189
|
-
sleep 2
|
190
|
-
end
|
191
|
-
puts
|
192
|
-
|
193
|
-
Certificate.by_issuance(order.certificate, csr)
|
194
|
-
end
|
195
|
-
|
196
|
-
def process_authorizations(authzs)
|
197
|
-
return if authzs.empty?
|
198
|
-
|
199
|
-
targets = authzs.map do |authz|
|
200
|
-
challenges = authz.challenges
|
201
|
-
challenge = nil
|
202
|
-
responder = config.challenge_responders.find do |x|
|
203
|
-
challenge = challenges.find { |c|
|
204
|
-
# OMG, acme-client might return a Hash instead of Acme::Client::Resources::Challenge::* object...
|
205
|
-
challenge_type = c.is_a?(Hash) ? c[:challenge_type] : c.challenge_type
|
206
|
-
x.support?(challenge_type)
|
207
|
-
}
|
208
|
-
end
|
209
|
-
{domain: authz.domain, authz: authz, responder: responder, responder_id: responder.__id__, challenge: challenge}
|
210
|
-
end
|
211
|
-
target_by_responders = targets.group_by{ |_| _.fetch(:responder_id) }.map { |_, ts| [ts[0].fetch(:responder), ts] }
|
212
|
-
|
213
|
-
begin
|
214
|
-
target_by_responders.each do |responder, ts|
|
215
|
-
puts "=> Responsing to the challenges for the following identifier:"
|
216
|
-
puts
|
217
|
-
puts " * Responder: #{responder.class}"
|
218
|
-
puts " * Identifiers:"
|
219
|
-
ts.each do |target|
|
220
|
-
puts " - #{target.fetch(:domain)} (#{target.fetch(:challenge).challenge_type})"
|
221
|
-
end
|
222
|
-
puts
|
223
|
-
|
224
|
-
responder.respond_all(*ts.map{ |t| [t.fetch(:domain), t.fetch(:challenge)] })
|
225
|
-
end
|
226
|
-
|
227
|
-
puts "=> Requesting validations..."
|
228
|
-
puts
|
229
|
-
targets.each do |target|
|
230
|
-
print " * #{target[:domain]} (#{target[:challenge].challenge_type}) ..."
|
231
|
-
target[:challenge].request_validation()
|
232
|
-
puts " [ ok ]"
|
233
|
-
end
|
234
|
-
puts
|
235
|
-
|
236
|
-
puts "=> Waiting for the validation..."
|
237
|
-
puts
|
238
|
-
|
239
|
-
loop do
|
240
|
-
all_valid = true
|
241
|
-
any_error = false
|
242
|
-
targets.each do |target|
|
243
|
-
next if target[:valid]
|
244
|
-
|
245
|
-
target[:challenge].reload
|
246
|
-
status = target[:challenge].status
|
247
|
-
|
248
|
-
puts " * [#{target[:domain]}] status: #{status}"
|
249
|
-
|
250
|
-
if status == 'valid'
|
251
|
-
target[:valid] = true
|
252
|
-
next
|
253
|
-
end
|
254
|
-
|
255
|
-
all_valid = false
|
256
|
-
if status == 'invalid'
|
257
|
-
any_error = true
|
258
|
-
err = target[:challenge].error
|
259
|
-
puts " ! [#{target[:domain]}] error: #{err.inspect}"
|
260
|
-
end
|
261
|
-
end
|
262
|
-
break if all_valid || any_error
|
263
|
-
sleep 3
|
264
|
-
end
|
265
|
-
puts
|
266
|
-
|
267
|
-
target_by_responders.each do |responder, ts|
|
268
|
-
puts "=> Cleaning the responses the challenges for the following identifier:"
|
269
|
-
puts
|
270
|
-
puts " * Responder: #{responder.class}"
|
271
|
-
puts " * Identifiers:"
|
272
|
-
ts.each do |target|
|
273
|
-
puts " - #{target.fetch(:domain)} (#{target.fetch(:challenge).challenge_type})"
|
274
|
-
end
|
275
|
-
puts
|
276
|
-
|
277
|
-
responder.cleanup_all(*ts.map{ |t| [t.fetch(:domain), t.fetch(:challenge)] })
|
278
|
-
end
|
279
|
-
|
280
|
-
puts "=> Authorized!"
|
281
|
-
end
|
282
|
-
end
|
283
165
|
|
284
166
|
def config
|
285
167
|
@config
|
@@ -296,7 +178,7 @@ module Acmesmith
|
|
296
178
|
end
|
297
179
|
|
298
180
|
def acme
|
299
|
-
@acme ||= Acme::Client.new(private_key: account_key.private_key, directory: config.
|
181
|
+
@acme ||= Acme::Client.new(private_key: account_key.private_key, directory: config.directory, connection_options: config.connection_options, bad_nonce_retry: config.bad_nonce_retry)
|
300
182
|
end
|
301
183
|
|
302
184
|
def certificate_key_passphrase
|
data/lib/acmesmith/config.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
require 'acmesmith/storages'
|
3
3
|
require 'acmesmith/challenge_responders'
|
4
|
+
require 'acmesmith/challenge_responder_filter'
|
4
5
|
require 'acmesmith/post_issuing_hooks'
|
5
6
|
|
6
7
|
module Acmesmith
|
7
8
|
class Config
|
9
|
+
ChallengeResponderRule = Struct.new(:challenge_responder, :filter, keyword_init: true)
|
10
|
+
|
8
11
|
def self.load_yaml(path)
|
9
12
|
new YAML.load_file(path)
|
10
13
|
end
|
@@ -40,6 +43,18 @@ module Acmesmith
|
|
40
43
|
@config.merge!(pair)
|
41
44
|
end
|
42
45
|
|
46
|
+
def directory
|
47
|
+
@config.fetch('directory')
|
48
|
+
end
|
49
|
+
|
50
|
+
def connection_options
|
51
|
+
@config['connection_options'] || {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def bad_nonce_retry
|
55
|
+
@config['bad_nonce_retry'] || 0
|
56
|
+
end
|
57
|
+
|
43
58
|
def account_key_passphrase
|
44
59
|
@config['account_key_passphrase']
|
45
60
|
end
|
@@ -76,8 +91,14 @@ module Acmesmith
|
|
76
91
|
@challenge_responders ||= begin
|
77
92
|
specs = @config['challenge_responders'].kind_of?(Hash) ? @config['challenge_responders'].map { |k,v| [k => v] } : @config['challenge_responders']
|
78
93
|
specs.flat_map do |specs_sub|
|
79
|
-
specs_sub
|
80
|
-
|
94
|
+
specs_sub = specs_sub.dup
|
95
|
+
filter = (specs_sub.delete('filter') || {}).map { |k,v| [k.to_sym, v] }.to_h
|
96
|
+
specs_sub.map do |k,v|
|
97
|
+
responder = ChallengeResponders.find(k).new(**v.map{ |k_,v_| [k_.to_sym, v_]}.to_h)
|
98
|
+
ChallengeResponderRule.new(
|
99
|
+
challenge_responder: responder,
|
100
|
+
filter: ChallengeResponderFilter.new(responder, **filter),
|
101
|
+
)
|
81
102
|
end
|
82
103
|
end
|
83
104
|
end
|