blob: c758ec71d042a0c6fb6619c6c6965d7ffc2bbbe5 (
plain)
| 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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
 | module Ievkit
  # Custom error class for rescuing from all Iev errors
  class Error < StandardError
    # Returns the appropriate Ievkit::Error subclass based
    # on status and response message
    #
    # @param [Hash] response HTTP response
    # @return [Ievkit::Error]
    def self.from_response(response)
      status  = response[:status].to_i
      body    = response[:body].to_s
      headers = response[:response_headers]
      if klass =  case status
                  when 400      then Ievkit::BadRequest
                  when 401      then error_for_401(headers)
                  when 403      then error_for_403(body)
                  when 404      then Ievkit::NotFound
                  when 405      then Ievkit::MethodNotAllowed
                  when 406      then Ievkit::NotAcceptable
                  when 409      then Ievkit::Conflict
                  when 415      then Ievkit::UnsupportedMediaType
                  when 422      then Ievkit::UnprocessableEntity
                  when 400..499 then Ievkit::ClientError
                  when 500      then Ievkit::InternalServerError
                  when 501      then Ievkit::NotImplemented
                  when 502      then Ievkit::BadGateway
                  when 503      then Ievkit::ServiceUnavailable
                  when 500..599 then Ievkit::ServerError
                  end
        klass.new(response)
      end
    end
    def initialize(response=nil)
      @response = response
      super(build_error_message)
    end
    # Documentation URL returned by the API for some errors
    #
    # @return [String]
    def documentation_url
      data[:documentation_url] if data.is_a? Hash
    end
    # Returns most appropriate error for 401 HTTP status code
    # @private
    def self.error_for_401(headers)
      if Ievkit::OneTimePasswordRequired.required_header(headers)
        Ievkit::OneTimePasswordRequired
      else
        Ievkit::Unauthorized
      end
    end
    # Returns most appropriate error for 403 HTTP status code
    # @private
    def self.error_for_403(body)
      if body =~ /rate limit exceeded/i
        Ievkit::TooManyRequests
      elsif body =~ /login attempts exceeded/i
        Ievkit::TooManyLoginAttempts
      elsif body =~ /abuse/i
        Ievkit::AbuseDetected
      elsif body =~ /repository access blocked/i
        Ievkit::RepositoryUnavailable
      else
        Ievkit::Forbidden
      end
    end
    # Array of validation errors
    # @return [Array<Hash>] Error info
    def errors
      if data && data.is_a?(Hash)
        data[:errors] || []
      else
        []
      end
    end
    private
    def data
      @data ||=
        if (body = @response[:body]) && !body.empty?
          if body.is_a?(String) &&
            @response[:response_headers] &&
            @response[:response_headers][:content_type] =~ /json/
            Sawyer::Agent.serializer.decode(body)
          else
            body
          end
        else
          nil
        end
    end
    def response_message
      case data
      when Hash
        data[:message]
      when String
        data
      end
    end
    def response_error
      "Error: #{data[:error]}" if data.is_a?(Hash) && data[:error]
    end
    def response_error_summary
      return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty?
      summary = "\nError summary:\n"
      summary << data[:errors].map do |hash|
        hash.map { |k,v| "  #{k}: #{v}" }
      end.join("\n")
      summary
    end
    def build_error_message
      return nil if @response.nil?
      message =  "#{@response[:method].to_s.upcase} "
      message << redact_url(@response[:url].to_s) + ": "
      message << "#{@response[:status]} - "
      message << "#{response_message}" unless response_message.nil?
      message << "#{response_error}" unless response_error.nil?
      message << "#{response_error_summary}" unless response_error_summary.nil?
      message << " // See: #{documentation_url}" unless documentation_url.nil?
      message
    end
    def redact_url(url_string)
      %w[client_secret access_token].each do |token|
        url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)") if url_string.include? token
      end
      url_string
    end
  end
  # Raised on errors in the 400-499 range
  class ClientError < Error; end
  # Raised when Iev returns a 400 HTTP status code
  class BadRequest < ClientError; end
  # Raised when Iev returns a 401 HTTP status code
  class Unauthorized < ClientError; end
  # Raised when Iev returns a 401 HTTP status code
  # and headers include "X-Iev-OTP"
  class OneTimePasswordRequired < ClientError
    #@private
    OTP_DELIVERY_PATTERN = /required; (\w+)/i
    #@private
    def self.required_header(headers)
      OTP_DELIVERY_PATTERN.match headers['X-Iev-OTP'].to_s
    end
    # Delivery method for the user's OTP
    #
    # @return [String]
    def password_delivery
      @password_delivery ||= delivery_method_from_header
    end
    private
    def delivery_method_from_header
      if match = self.class.required_header(@response[:response_headers])
        match[1]
      end
    end
  end
  # Raised when Iev returns a 403 HTTP status code
  class Forbidden < ClientError; end
  # Raised when Iev returns a 403 HTTP status code
  # and body matches 'rate limit exceeded'
  class TooManyRequests < Forbidden; end
  # Raised when Iev returns a 403 HTTP status code
  # and body matches 'login attempts exceeded'
  class TooManyLoginAttempts < Forbidden; end
  # Raised when Iev returns a 403 HTTP status code
  # and body matches 'abuse'
  class AbuseDetected < Forbidden; end
  # Raised when Iev returns a 403 HTTP status code
  # and body matches 'repository access blocked'
  class RepositoryUnavailable < Forbidden; end
  # Raised when Iev returns a 404 HTTP status code
  class NotFound < ClientError; end
  # Raised when Iev returns a 405 HTTP status code
  class MethodNotAllowed < ClientError; end
  # Raised when Iev returns a 406 HTTP status code
  class NotAcceptable < ClientError; end
  # Raised when Iev returns a 409 HTTP status code
  class Conflict < ClientError; end
  # Raised when Iev returns a 414 HTTP status code
  class UnsupportedMediaType < ClientError; end
  # Raised when Iev returns a 422 HTTP status code
  class UnprocessableEntity < ClientError; end
  # Raised on errors in the 500-599 range
  class ServerError < Error; end
  # Raised when Iev returns a 500 HTTP status code
  class InternalServerError < ServerError; end
  # Raised when Iev returns a 501 HTTP status code
  class NotImplemented < ServerError; end
  # Raised when Iev returns a 502 HTTP status code
  class BadGateway < ServerError; end
  # Raised when Iev returns a 503 HTTP status code
  class ServiceUnavailable < ServerError; end
  # Raised when client fails to provide valid Content-Type
  class MissingContentType < ArgumentError; end
  # Raised when a method requires an application client_id
  # and secret but none is provided
  class ApplicationCredentialsRequired < StandardError; end
end
 |