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 34 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_url = URI(directory) @nonces ||= [] end
Public Instance Methods
account()
click to toggle source
# File lib/acme/client.rb, line 123 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 95 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 101 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 85 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 248 def caa_identities directory.caa_identities end
certificate(url:, force_chain: nil)
click to toggle source
# File lib/acme/client.rb, line 166 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 198 def challenge(url:) response = post_as_get(url) arguments = attributes_from_challenge_response(response) Acme::Client::Resources::Challenges.new(self, **arguments) end
directory()
click to toggle source
# File lib/acme/client.rb, line 232 def directory @directory ||= load_directory end
external_account_required()
click to toggle source
# File lib/acme/client.rb, line 252 def external_account_required directory.external_account_required end
finalize(url:, csr:)
click to toggle source
# File lib/acme/client.rb, line 155 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 225 def get_nonce http_client = Acme::Client::HTTPClient.new_connection(url: endpoint_for(:new_nonce), options: @connection_options) response = http_client.head(nil, nil) nonces << response.headers['replay-nonce'] true end
kid()
click to toggle source
# File lib/acme/client.rb, line 134 def kid @kid ||= account.kid end
meta()
click to toggle source
# File lib/acme/client.rb, line 236 def meta directory.meta end
new_account(contact:, terms_of_service_agreed: nil, external_account_binding: nil)
click to toggle source
# File lib/acme/client.rb, line 53 def new_account(contact:, terms_of_service_agreed: nil, external_account_binding: nil) new_account_endpoint = endpoint_for(:new_account) payload = { contact: Array(contact) } if terms_of_service_agreed payload[:termsOfServiceAgreed] = terms_of_service_agreed end if external_account_binding kid, hmac_key = external_account_binding.values_at(:kid, :hmac_key) if kid.nil? || hmac_key.nil? raise ArgumentError, 'must specify kid and hmac_key key for external_account_binding' end hmac = Acme::Client::JWK::HMAC.new(Base64.urlsafe_decode64(hmac_key)) external_account_payload = hmac.jws(header: { kid: kid, url: new_account_endpoint }, payload: @jwk) payload[:externalAccountBinding] = JSON.parse(external_account_payload) end response = post(new_account_endpoint, 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 138 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 149 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 204 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 210 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 240 def terms_of_service directory.terms_of_service end
website()
click to toggle source
# File lib/acme/client.rb, line 244 def website directory.website end
Private Instance Methods
attributes_from_account_response(response)
click to toggle source
# File lib/acme/client.rb, line 284 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 312 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 293 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 348 def connection_for(url:, mode:) uri = URI(url) endpoint = "#{uri.scheme}://#{uri.hostname}:#{uri.port}" @connections ||= {} @connections[mode] ||= {} @connections[mode][endpoint] ||= Acme::Client::HTTPClient.new_acme_connection( url: URI(endpoint), mode: mode, client: self, options: @connection_options, bad_nonce_retry: @bad_nonce_retry ) end
download(url, format:)
click to toggle source
# File lib/acme/client.rb, line 340 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 359 def endpoint_for(key) directory.endpoint_for(key) end
extract_attributes(input, *attributes)
click to toggle source
# File lib/acme/client.rb, line 316 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_directory()
click to toggle source
# File lib/acme/client.rb, line 262 def fetch_directory response = get(@directory_url) response.body rescue JSON::ParserError => exception raise Acme::Client::Error::InvalidDirectory, "Invalid directory url\n#{@directory_url} did not return a valid directory\n#{exception.inspect}" end
get(url, mode: :get)
click to toggle source
# File lib/acme/client.rb, line 335 def get(url, mode: :get) connection = connection_for(url: url, mode: mode) connection.get(url) end
load_directory()
click to toggle source
# File lib/acme/client.rb, line 258 def load_directory Acme::Client::Resources::Directory.new(self, directory: fetch_directory) end
post(url, payload: {}, mode: :kid)
click to toggle source
# File lib/acme/client.rb, line 325 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 330 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 270 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