class Tipi::ACME::CertificateManager
Constants
- ACME_CHALLENGE_PATH_REGEXP
- ACME_DIRECTORY
- CERTIFICATE_REGEXP
- IP_REGEXP
- LOCALHOST_REGEXP
Public Class Methods
new(master_ctx:, store:, challenge_handler:)
click to toggle source
# File lib/tipi/acme.rb, line 13 def initialize(master_ctx:, store:, challenge_handler:) @master_ctx = master_ctx @store = store @challenge_handler = challenge_handler @contexts = {} @requests = Polyphony::Queue.new @worker = spin { run } setup_sni_callback end
Public Instance Methods
acme_client()
click to toggle source
# File lib/tipi/acme.rb, line 136 def acme_client @acme_client ||= setup_acme_client end
challenge_routing_app(app)
click to toggle source
# File lib/tipi/acme.rb, line 25 def challenge_routing_app(app) ->(req) do (req.path =~ ACME_CHALLENGE_PATH_REGEXP ? @challenge_handler : app) .(req) rescue => e puts "Error while handling request: #{e.inspect} (headers: #{req.headers})" req.respond(nil, ':status' => Qeweney::Status::BAD_REQUEST) end end
get_certificate(name)
click to toggle source
# File lib/tipi/acme.rb, line 116 def get_certificate(name) entry = @store.get(name) return entry if entry provision_certificate(name).tap do |entry| @store.set(name, **entry) end end
get_context(name)
click to toggle source
# File lib/tipi/acme.rb, line 75 def get_context(name) @contexts[name] = setup_context(name) end
get_expired_stamp(certificate)
click to toggle source
# File lib/tipi/acme.rb, line 110 def get_expired_stamp(certificate) chain = parse_certificate(certificate) cert = chain.shift cert.not_after end
localhost_context()
click to toggle source
# File lib/tipi/acme.rb, line 125 def localhost_context @localhost_authority ||= Localhost::Authority.fetch @localhost_authority.server_context end
parse_certificate(certificate)
click to toggle source
# File lib/tipi/acme.rb, line 104 def parse_certificate(certificate) certificate .scan(CERTIFICATE_REGEXP) .map { |p| OpenSSL::X509::Certificate.new(p.first) } end
private_key()
click to toggle source
# File lib/tipi/acme.rb, line 130 def private_key @private_key ||= OpenSSL::PKey::RSA.new(4096) end
provision_certificate(name)
click to toggle source
# File lib/tipi/acme.rb, line 152 def provision_certificate(name) p provision_certificate: name order = acme_client.new_order(identifiers: [name]) authorization = order.authorizations.first challenge = authorization.http @challenge_handler.add(challenge) challenge.request_validation while challenge.status == 'pending' sleep(0.25) challenge.reload end raise ACME::Error, "Invalid CSR" if challenge.status == 'invalid' p challenge_status: challenge.status private_key = OpenSSL::PKey::RSA.new(4096) csr = Acme::Client::CertificateRequest.new( private_key: private_key, subject: { common_name: name } ) order.finalize(csr: csr) while order.status == 'processing' sleep(0.25) order.reload end certificate = begin order.certificate(force_chain: 'DST Root CA X3') rescue Acme::Client::Error::ForcedChainNotFound order.certificate end expired_stamp = get_expired_stamp(certificate) puts "Certificate for #{name} expires: #{expired_stamp.inspect}" { private_key: private_key, certificate: certificate, expired_stamp: expired_stamp } end
provision_context(name)
click to toggle source
# File lib/tipi/acme.rb, line 85 def provision_context(name) return localhost_context if name =~ LOCALHOST_REGEXP info = get_certificate(name) ctx = OpenSSL::SSL::SSLContext.new chain = parse_certificate(info[:certificate]) cert = chain.shift ctx.add_certificate(cert, info[:private_key], chain) ctx end
run()
click to toggle source
# File lib/tipi/acme.rb, line 64 def run loop do name, state = @requests.shift state[:ctx] = get_context(name) rescue => e state[:error] = e if state end end
setup_acme_client()
click to toggle source
# File lib/tipi/acme.rb, line 140 def setup_acme_client client = Acme::Client.new( private_key: private_key, directory: ACME_DIRECTORY ) account = client.new_account( contact: 'mailto:info@noteflakes.com', terms_of_service_agreed: true ) client end
setup_context(name)
click to toggle source
# File lib/tipi/acme.rb, line 79 def setup_context(name) ctx = provision_context(name) transfer_ctx_settings(ctx) ctx end
setup_sni_callback()
click to toggle source
# File lib/tipi/acme.rb, line 37 def setup_sni_callback @master_ctx.servername_cb = proc do |_socket, name| p servername_cb: name state = { ctx: nil } if name =~ IP_REGEXP @master_ctx else @requests << [name, state] wait_for_ctx(state) p name: name, error: state if state[:error] # Eventually we might want to return an error returned in # state[:error]. For the time being we handle errors by returning the # master context state[:ctx] || @master_ctx end end end
transfer_ctx_settings(ctx)
click to toggle source
# File lib/tipi/acme.rb, line 96 def transfer_ctx_settings(ctx) ctx.alpn_protocols = @master_ctx.alpn_protocols ctx.alpn_select_cb = @master_ctx.alpn_select_cb ctx.ciphers = @master_ctx.ciphers end
wait_for_ctx(state)
click to toggle source
# File lib/tipi/acme.rb, line 56 def wait_for_ctx(state) period = 0.00001 while !state[:ctx] && !state[:error] orig_sleep period period *= 2 if period < 0.1 end end