class Acme::Client
Constants
- CONTENT_TYPES
- DEFAULT_DIRECTORY
- USER_AGENT
- VERSION
Attributes
jwk[R]
nonces[R]
Public Class Methods
new(jwk: nil, kid: nil, private_key: nil, directory: DEFAULT_DIRECTORY, connection_options: {}, bad_nonce_retry: 0)
click to toggle source
# File lib/acme/client.rb, line 33 def initialize(jwk: nil, kid: nil, private_key: nil, directory: DEFAULT_DIRECTORY, connection_options: {}, bad_nonce_retry: 0) if jwk.nil? && private_key.nil? raise ArgumentError, 'must specify jwk or private_key' end @jwk = if jwk jwk else Acme::Client::JWK.from_private_key(private_key) end @kid, @connection_options = kid, connection_options @bad_nonce_retry = bad_nonce_retry @directory = Acme::Client::Resources::Directory.new(URI(directory), @connection_options) @nonces ||= [] end
Public Instance Methods
account()
click to toggle source
# File lib/acme/client.rb, line 110 def account @kid ||= begin response = post(endpoint_for(:new_account), payload: { onlyReturnExisting: true }, mode: :jwk) response.headers.fetch(:location) end response = post_as_get(@kid) arguments = attributes_from_account_response(response) Acme::Client::Resources::Account.new(self, url: @kid, **arguments) end
account_deactivate()
click to toggle source
# File lib/acme/client.rb, line 82 def account_deactivate response = post(kid, payload: { status: 'deactivated' }) arguments = attributes_from_account_response(response) Acme::Client::Resources::Account.new(self, url: kid, **arguments) end
account_key_change(new_private_key: nil, new_jwk: nil)
click to toggle source
# File lib/acme/client.rb, line 88 def account_key_change(new_private_key: nil, new_jwk: nil) if new_private_key.nil? && new_jwk.nil? raise ArgumentError, 'must specify new_jwk or new_private_key' end old_jwk = jwk new_jwk ||= Acme::Client::JWK.from_private_key(new_private_key) inner_payload_header = { url: endpoint_for(:key_change) } inner_payload = { account: kid, oldKey: old_jwk.to_h } payload = JSON.parse(new_jwk.jws(header: inner_payload_header, payload: inner_payload)) response = post(endpoint_for(:key_change), payload: payload, mode: :kid) arguments = attributes_from_account_response(response) @jwk = new_jwk Acme::Client::Resources::Account.new(self, url: kid, **arguments) end
account_update(contact: nil, terms_of_service_agreed: nil)
click to toggle source
# File lib/acme/client.rb, line 72 def account_update(contact: nil, terms_of_service_agreed: nil) payload = {} payload[:contact] = Array(contact) if contact payload[:termsOfServiceAgreed] = terms_of_service_agreed if terms_of_service_agreed response = post(kid, payload: payload) arguments = attributes_from_account_response(response) Acme::Client::Resources::Account.new(self, url: kid, **arguments) end
caa_identities()
click to toggle source
# File lib/acme/client.rb, line 231 def caa_identities @directory.caa_identities end
certificate(url:, force_chain: nil)
click to toggle source
# File lib/acme/client.rb, line 153 def certificate(url:, force_chain: nil) response = download(url, format: :pem) pem = response.body return pem if force_chain.nil? return pem if ChainIdentifier.new(pem).match_name?(force_chain) alternative_urls = Array(response.headers.dig('link', 'alternate')) alternative_urls.each do |alternate_url| response = download(alternate_url, format: :pem) pem = response.body if ChainIdentifier.new(pem).match_name?(force_chain) return pem end end raise Acme::Client::Error::ForcedChainNotFound, "Could not find any matching chain for `#{force_chain}`" end
challenge(url:)
click to toggle source
# File lib/acme/client.rb, line 185 def challenge(url:) response = post_as_get(url) arguments = attributes_from_challenge_response(response) Acme::Client::Resources::Challenges.new(self, **arguments) end
external_account_required()
click to toggle source
# File lib/acme/client.rb, line 235 def external_account_required @directory.external_account_required end
finalize(url:, csr:)
click to toggle source
# File lib/acme/client.rb, line 142 def finalize(url:, csr:) unless csr.respond_to?(:to_der) raise ArgumentError, 'csr must respond to `#to_der`' end base64_der_csr = Acme::Client::Util.urlsafe_base64(csr.to_der) response = post(url, payload: { csr: base64_der_csr }) arguments = attributes_from_order_response(response) Acme::Client::Resources::Order.new(self, **arguments) end
get_nonce()
click to toggle source
# File lib/acme/client.rb, line 212 def get_nonce connection = new_connection(endpoint: endpoint_for(:new_nonce)) response = connection.head(nil, nil, 'User-Agent' => USER_AGENT) nonces << response.headers['replay-nonce'] true end
kid()
click to toggle source
# File lib/acme/client.rb, line 121 def kid @kid ||= account.kid end
meta()
click to toggle source
# File lib/acme/client.rb, line 219 def meta @directory.meta end
new_account(contact:, terms_of_service_agreed: nil)
click to toggle source
# File lib/acme/client.rb, line 52 def new_account(contact:, terms_of_service_agreed: nil) payload = { contact: Array(contact) } if terms_of_service_agreed payload[:termsOfServiceAgreed] = terms_of_service_agreed end response = post(endpoint_for(:new_account), payload: payload, mode: :jws) @kid = response.headers.fetch(:location) if response.body.nil? || response.body.empty? account else arguments = attributes_from_account_response(response) Acme::Client::Resources::Account.new(self, url: @kid, **arguments) end end
new_order(identifiers:, not_before: nil, not_after: nil)
click to toggle source
# File lib/acme/client.rb, line 125 def new_order(identifiers:, not_before: nil, not_after: nil) payload = {} payload['identifiers'] = prepare_order_identifiers(identifiers) payload['notBefore'] = not_before if not_before payload['notAfter'] = not_after if not_after response = post(endpoint_for(:new_order), payload: payload) arguments = attributes_from_order_response(response) Acme::Client::Resources::Order.new(self, **arguments) end
order(url:)
click to toggle source
# File lib/acme/client.rb, line 136 def order(url:) response = post_as_get(url) arguments = attributes_from_order_response(response) Acme::Client::Resources::Order.new(self, **arguments.merge(url: url)) end
request_challenge_validation(url:, key_authorization: nil)
click to toggle source
# File lib/acme/client.rb, line 191 def request_challenge_validation(url:, key_authorization: nil) response = post(url, payload: {}) arguments = attributes_from_challenge_response(response) Acme::Client::Resources::Challenges.new(self, **arguments) end
revoke(certificate:, reason: nil)
click to toggle source
# File lib/acme/client.rb, line 197 def revoke(certificate:, reason: nil) der_certificate = if certificate.respond_to?(:to_der) certificate.to_der else OpenSSL::X509::Certificate.new(certificate).to_der end base64_der_certificate = Acme::Client::Util.urlsafe_base64(der_certificate) payload = { certificate: base64_der_certificate } payload[:reason] = reason unless reason.nil? response = post(endpoint_for(:revoke_certificate), payload: payload) response.success? end
terms_of_service()
click to toggle source
# File lib/acme/client.rb, line 223 def terms_of_service @directory.terms_of_service end
website()
click to toggle source
# File lib/acme/client.rb, line 227 def website @directory.website end
Private Instance Methods
attributes_from_account_response(response)
click to toggle source
# File lib/acme/client.rb, line 255 def attributes_from_account_response(response) extract_attributes( response.body, :status, [:term_of_service, 'termsOfServiceAgreed'], :contact ) end
attributes_from_challenge_response(response)
click to toggle source
# File lib/acme/client.rb, line 283 def attributes_from_challenge_response(response) extract_attributes(response.body, :status, :url, :token, :type, :error) end
attributes_from_order_response(response)
click to toggle source
# File lib/acme/client.rb, line 264 def attributes_from_order_response(response) attributes = extract_attributes( response.body, :status, :expires, [:finalize_url, 'finalize'], [:authorization_urls, 'authorizations'], [:certificate_url, 'certificate'], :identifiers ) attributes[:url] = response.headers[:location] if response.headers[:location] attributes end
connection_for(url:, mode:)
click to toggle source
# File lib/acme/client.rb, line 319 def connection_for(url:, mode:) uri = URI(url) endpoint = "#{uri.scheme}://#{uri.hostname}:#{uri.port}" @connections ||= {} @connections[mode] ||= {} @connections[mode][endpoint] ||= new_acme_connection(endpoint: endpoint, mode: mode) end
download(url, format:)
click to toggle source
# File lib/acme/client.rb, line 311 def download(url, format:) connection = connection_for(url: url, mode: :kid) connection.post do |request| request.url(url) request.headers['Accept'] = CONTENT_TYPES.fetch(format) end end
endpoint_for(key)
click to toggle source
# File lib/acme/client.rb, line 356 def endpoint_for(key) @directory.endpoint_for(key) end
extract_attributes(input, *attributes)
click to toggle source
# File lib/acme/client.rb, line 287 def extract_attributes(input, *attributes) attributes .map {|fields| Array(fields) } .each_with_object({}) { |(key, field), hash| field ||= key.to_s hash[key] = input[field] } end
fetch_chain(response, limit = 10)
click to toggle source
# File lib/acme/client.rb, line 346 def fetch_chain(response, limit = 10) links = response.headers['link'] if limit.zero? || links.nil? || links['up'].nil? [] else issuer = get(links['up']) [OpenSSL::X509::Certificate.new(issuer.body), *fetch_chain(issuer, limit - 1)] end end
get(url, mode: :kid)
click to toggle source
# File lib/acme/client.rb, line 306 def get(url, mode: :kid) connection = connection_for(url: url, mode: mode) connection.get(url) end
new_acme_connection(endpoint:, mode:)
click to toggle source
# File lib/acme/client.rb, line 327 def new_acme_connection(endpoint:, mode:) new_connection(endpoint: endpoint) do |configuration| configuration.use Acme::Client::FaradayMiddleware, client: self, mode: mode end end
new_connection(endpoint:) { |configuration| ... }
click to toggle source
# File lib/acme/client.rb, line 333 def new_connection(endpoint:) Faraday.new(endpoint, **@connection_options) do |configuration| if @bad_nonce_retry > 0 configuration.request(:retry, max: @bad_nonce_retry, methods: Faraday::Connection::METHODS, exceptions: [Acme::Client::Error::BadNonce]) end yield(configuration) if block_given? configuration.adapter Faraday.default_adapter end end
post(url, payload: {}, mode: :kid)
click to toggle source
# File lib/acme/client.rb, line 296 def post(url, payload: {}, mode: :kid) connection = connection_for(url: url, mode: mode) connection.post(url, payload) end
post_as_get(url, mode: :kid)
click to toggle source
# File lib/acme/client.rb, line 301 def post_as_get(url, mode: :kid) connection = connection_for(url: url, mode: mode) connection.post(url, nil) end
prepare_order_identifiers(identifiers)
click to toggle source
# File lib/acme/client.rb, line 241 def prepare_order_identifiers(identifiers) if identifiers.is_a?(Hash) [identifiers] else Array(identifiers).map do |identifier| if identifier.is_a?(String) { type: 'dns', value: identifier } else identifier end end end end