File: http_client.rb

package info (click to toggle)
ruby-acme-client 2.10.really.2.0.18-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,132 kB
  • sloc: ruby: 2,217; makefile: 7; sh: 3
file content (162 lines) | stat: -rw-r--r-- 4,155 bytes parent folder | download
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