1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
|
# frozen_string_literal: true
module Acme::Client::HTTPClient
# Creates and returns a new HTTP client, with default settings.
#
# @param url [URI:HTTPS]
# @param options [Hash]
# @return [Faraday::Connection]
def self.new_connection(url:, options: {})
Faraday.new(url, options) do |configuration|
configuration.use Acme::Client::HTTPClient::ErrorMiddleware
yield(configuration) if block_given?
configuration.headers[:user_agent] = Acme::Client::USER_AGENT
configuration.adapter Faraday.default_adapter
end
end
# Creates and returns a new HTTP client designed for the Acme-protocol, with default settings.
#
# @param url [URI:HTTPS]
# @param client [Acme::Client]
# @param mode [Symbol]
# @param options [Hash]
# @param bad_nonce_retry [Integer]
# @return [Faraday::Connection]
def self.new_acme_connection(url:, client:, mode:, options: {}, bad_nonce_retry: 0)
new_connection(url: url, options: 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
configuration.use Acme::Client::HTTPClient::AcmeMiddleware, client: client, mode: mode
yield(configuration) if block_given?
end
end
# ErrorMiddleware ensures the HTTP Client would not raise exceptions outside the Acme namespace.
#
# Exceptions are rescued and re-packaged as Acme exceptions.
class ErrorMiddleware < Faraday::Middleware
# Implements the Rack-alike Faraday::Middleware interface.
def call(env)
@app.call(env)
rescue Faraday::TimeoutError, Faraday::ConnectionFailed
raise Acme::Client::Error::Timeout
end
end
# AcmeMiddleware implements the Acme-protocol requirements for JWK requests.
class AcmeMiddleware < Faraday::Middleware
attr_reader :env, :response, :client
CONTENT_TYPE = 'application/jose+json'
def initialize(app, options)
super(app)
@client = options.fetch(:client)
@mode = options.fetch(:mode)
end
def call(env)
@env = env
@env[:request_headers]['Content-Type'] = CONTENT_TYPE
if @env.method != :get
@env.body = client.jwk.jws(header: jws_header, payload: env.body)
end
@app.call(env).on_complete { |response_env| on_complete(response_env) }
end
def on_complete(env)
@env = env
raise_on_not_found!
store_nonce
env.body = decode_body
env.response_headers['Link'] = decode_link_headers
return if env.success?
raise_on_error!
end
private
def jws_header
headers = { nonce: pop_nonce, url: env.url.to_s }
headers[:kid] = client.kid if @mode == :kid
headers
end
def raise_on_not_found!
raise Acme::Client::Error::NotFound, env.url.to_s if env.status == 404
end
def raise_on_error!
raise error_class, error_message
end
def error_message
if env.body.is_a? Hash
env.body['detail']
else
"Error message: #{env.body}"
end
end
def error_class
Acme::Client::Error::ACME_ERRORS.fetch(error_name, Acme::Client::Error)
end
def error_name
return unless env.body.is_a?(Hash)
return unless env.body.key?('type')
env.body['type']
end
def decode_body
content_type = env.response_headers['Content-Type'].to_s
if content_type.start_with?('application/json', 'application/problem+json')
JSON.load(env.body)
else
env.body
end
end
def decode_link_headers
return unless env.response_headers.key?('Link')
link_header = env.response_headers['Link']
Acme::Client::Util.decode_link_headers(link_header)
end
def store_nonce
nonce = env.response_headers['replay-nonce']
nonces << nonce if nonce
end
def pop_nonce
if nonces.empty?
get_nonce
end
nonces.pop
end
def get_nonce
client.get_nonce
end
def nonces
client.nonces
end
end
end
|