pingpp 2.0.15 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/pingpp.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  # Pingpp Ruby bindings
2
- # API spec at https://pingxx.com/document/api
2
+ # API spec at https://www.pingxx.com/api
3
3
  require 'cgi'
4
4
  require 'set'
5
5
  require 'openssl'
6
6
  require 'rest-client'
7
7
  require 'json'
8
8
  require 'base64'
9
+ require 'socket'
10
+ require 'rbconfig'
9
11
 
10
12
  # Version
11
13
  require 'pingpp/version'
@@ -15,6 +17,7 @@ require 'pingpp/api_operations/create'
15
17
  require 'pingpp/api_operations/update'
16
18
  require 'pingpp/api_operations/delete'
17
19
  require 'pingpp/api_operations/list'
20
+ require 'pingpp/api_operations/request'
18
21
 
19
22
  # Resources
20
23
  require 'pingpp/util'
@@ -29,6 +32,9 @@ require 'pingpp/event'
29
32
  require 'pingpp/transfer'
30
33
  require 'pingpp/webhook'
31
34
  require 'pingpp/identification'
35
+ require 'pingpp/customs'
36
+ require 'pingpp/batch_refund'
37
+ require 'pingpp/batch_transfer'
32
38
 
33
39
  # Errors
34
40
  require 'pingpp/errors/pingpp_error'
@@ -37,27 +43,45 @@ require 'pingpp/errors/api_connection_error'
37
43
  require 'pingpp/errors/invalid_request_error'
38
44
  require 'pingpp/errors/authentication_error'
39
45
  require 'pingpp/errors/channel_error'
46
+ require 'pingpp/errors/rate_limit_error'
40
47
 
41
48
  # WxPubOauth
42
49
  require 'pingpp/wx_pub_oauth'
43
50
 
44
51
  module Pingpp
45
52
  DEFAULT_CA_BUNDLE_PATH = File.dirname(__FILE__) + '/data/ca-certificates.crt'
46
- @api_base = 'https://api.pingxx.com'
47
53
 
48
- @api_version = '2015-10-10'
54
+ RETRY_EXCEPTIONS = [
55
+ Errno::ECONNREFUSED,
56
+ Errno::ECONNRESET,
57
+ Errno::ETIMEDOUT,
58
+ RestClient::Conflict,
59
+ RestClient::RequestTimeout,
60
+ ].freeze
61
+
62
+ @api_base = 'https://api.pingxx.com'
49
63
 
50
- @ssl_bundle_path = DEFAULT_CA_BUNDLE_PATH
64
+ @ca_bundle_path = DEFAULT_CA_BUNDLE_PATH
51
65
  @verify_ssl_certs = true
52
66
 
67
+ @open_timeout = 30
68
+ @timeout = 80
69
+
70
+ @max_network_retries = 0
71
+ @max_network_retry_delay = 2
72
+ @initial_network_retry_delay = 0.5
73
+
53
74
  HEADERS_TO_PARSE = [:pingpp_one_version, :pingpp_sdk_version]
54
75
 
55
76
  class << self
56
- attr_accessor :api_key, :api_base, :verify_ssl_certs, :api_version, :parsed_headers, :private_key, :pub_key
77
+ attr_accessor :api_key, :api_base, :verify_ssl_certs, :api_version,
78
+ :parsed_headers, :private_key, :pub_key, :app_id, :timeout,
79
+ :open_timeout
80
+ attr_reader :max_network_retry_delay, :initial_network_retry_delay
57
81
  end
58
82
 
59
- def self.api_url(url='')
60
- @api_base + url
83
+ def self.api_url(url='', api_base_url=nil)
84
+ (api_base_url || @api_base) + url
61
85
  end
62
86
 
63
87
  def self.parse_headers(headers)
@@ -77,7 +101,9 @@ module Pingpp
77
101
  end
78
102
  end
79
103
 
80
- def self.request(method, url, api_key, params={}, headers={})
104
+ def self.request(method, url, api_key, params={}, headers={}, api_base_url=nil)
105
+ api_base_url = api_base_url || @api_base
106
+
81
107
  unless api_key ||= @api_key
82
108
  raise AuthenticationError.new('No API key provided. ' +
83
109
  'Set your API key using "Pingpp.api_key = <API-KEY>". ' +
@@ -93,7 +119,7 @@ module Pingpp
93
119
 
94
120
  if verify_ssl_certs
95
121
  request_opts = {:verify_ssl => OpenSSL::SSL::VERIFY_PEER,
96
- :ssl_ca_file => @ssl_bundle_path,
122
+ :ssl_ca_file => @ca_bundle_path,
97
123
  :ssl_version => 'TLSv1'}
98
124
  else
99
125
  request_opts = {:verify_ssl => false,
@@ -107,46 +133,35 @@ module Pingpp
107
133
  end
108
134
 
109
135
  params = Util.objects_to_ids(params)
110
- url = api_url(url)
136
+ url = api_url(url, api_base_url)
111
137
 
112
- case method.to_s.downcase.to_sym
138
+ method_sym = method.to_s.downcase.to_sym
139
+ case method_sym
113
140
  when :get, :head, :delete
114
141
  # Make params into GET parameters
115
- url += "#{URI.parse(url).query ? '&' : '?'}#{uri_encode(params)}" if params && params.any?
142
+ url += "#{URI.parse(url).query ? '&' : '?'}#{Util.encode_parameters(params)}" if params && params.any?
116
143
  payload = nil
117
144
  else
118
145
  payload = JSON.generate(params)
119
146
  end
120
147
 
121
- request_opts.update(:headers => request_headers(api_key, method.to_s.downcase.to_sym, payload).update(headers),
122
- :method => method, :open_timeout => 30,
123
- :payload => payload, :url => url, :timeout => 80)
148
+ request_opts.update(:headers => request_headers(api_key, method_sym, payload, url).update(headers),
149
+ :method => method, :open_timeout => open_timeout,
150
+ :payload => payload, :url => url, :timeout => timeout)
124
151
 
125
- begin
126
- response = execute_request(request_opts)
127
- rescue SocketError => e
128
- handle_restclient_error(e)
129
- rescue NoMethodError => e
130
- # Work around RestClient bug
131
- if e.message =~ /\WRequestFailed\W/
132
- e = APIConnectionError.new('Unexpected HTTP response code')
133
- handle_restclient_error(e)
134
- else
135
- raise
136
- end
137
- rescue RestClient::ExceptionWithResponse => e
138
- if rcode = e.http_code and rbody = e.http_body
139
- handle_api_error(rcode, rbody)
140
- else
141
- handle_restclient_error(e)
142
- end
143
- rescue RestClient::Exception, Errno::ECONNREFUSED => e
144
- handle_restclient_error(e)
145
- end
152
+ response = execute_request_with_rescues(request_opts, api_base_url)
146
153
 
147
154
  [parse(response), api_key]
148
155
  end
149
156
 
157
+ def self.max_network_retries
158
+ @max_network_retries
159
+ end
160
+
161
+ def self.max_network_retries=(val)
162
+ @max_network_retries = val.to_i
163
+ end
164
+
150
165
  private
151
166
 
152
167
  def self.user_agent
@@ -159,23 +174,75 @@ module Pingpp
159
174
  :lang_version => lang_version,
160
175
  :platform => RUBY_PLATFORM,
161
176
  :publisher => 'pingpp',
162
- :uname => @uname
177
+ :uname => @uname,
178
+ :hostname => Socket.gethostname,
179
+ :engine => defined?(RUBY_ENGINE) ? RUBY_ENGINE : '',
180
+ :openssl_version => OpenSSL::OPENSSL_VERSION
163
181
  }
164
182
 
165
183
  end
166
184
 
167
185
  def self.get_uname
168
- `uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
169
- rescue Errno::ENOMEM => ex # couldn't create subprocess
186
+ if File.exist?('/proc/version')
187
+ File.read('/proc/version').strip
188
+ else
189
+ case RbConfig::CONFIG['host_os']
190
+ when /linux|darwin|bsd|sunos|solaris|cygwin/i
191
+ _uname_uname
192
+ when /mswin|mingw/i
193
+ _uname_ver
194
+ else
195
+ "unknown platform"
196
+ end
197
+ end
198
+ end
199
+
200
+ def self._uname_uname
201
+ (`uname -a 2>/dev/null` || '').strip
202
+ rescue Errno::ENOMEM
170
203
  "uname lookup failed"
171
204
  end
172
205
 
173
- def self.uri_encode(params)
174
- Util.flatten_params(params).
175
- map { |k,v| "#{k}=#{Util.url_encode(v)}" }.join('&')
206
+ def self._uname_ver
207
+ (`ver` || '').strip
208
+ rescue Errno::ENOMEM
209
+ "uname lookup failed"
176
210
  end
177
211
 
178
- def self.request_headers(api_key, method_sym, data=nil)
212
+ def self.execute_request_with_rescues(request_opts, api_base_url, retry_count = 0)
213
+ begin
214
+ response = execute_request(request_opts)
215
+
216
+ rescue => e
217
+ if should_retry?(e, retry_count)
218
+ retry_count = retry_count + 1
219
+ sleep sleep_time(retry_count)
220
+ retry
221
+ end
222
+
223
+ case e
224
+ when SocketError
225
+ response = handle_restclient_error(e, request_opts, retry_count, api_base_url)
226
+
227
+ when RestClient::ExceptionWithResponse
228
+ if e.response
229
+ handle_api_error(e.response)
230
+ else
231
+ response = handle_restclient_error(e, request_opts, retry_count, api_base_url)
232
+ end
233
+
234
+ when RestClient::Exception, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError
235
+ response = handle_restclient_error(e, request_opts, retry_count, api_base_url)
236
+
237
+ else
238
+ raise
239
+ end
240
+ end
241
+
242
+ response
243
+ end
244
+
245
+ def self.request_headers(api_key, method_sym, data, url)
179
246
  post_or_put = (method_sym == :post or method_sym == :put)
180
247
  headers = {
181
248
  :user_agent => "Pingpp/v1 RubyBindings/#{Pingpp::VERSION}",
@@ -193,8 +260,17 @@ module Pingpp
193
260
  :error => "#{e} (#{e.class})")
194
261
  end
195
262
 
196
- if post_or_put && private_key && data
197
- signature = sign_request(data, private_key)
263
+ data_to_be_signed = data || ''
264
+ uri = URI.parse(url)
265
+ data_to_be_signed += uri.path
266
+ (!uri.query.nil?) && data_to_be_signed += '?' + uri.query
267
+
268
+ request_time = Time.now.to_i.to_s
269
+ headers.update(:pingplusplus_request_timestamp => request_time)
270
+ data_to_be_signed += request_time
271
+
272
+ if private_key
273
+ signature = sign_request(data_to_be_signed, private_key)
198
274
  headers.update(:pingplusplus_signature => signature)
199
275
  end
200
276
 
@@ -235,63 +311,81 @@ module Pingpp
235
311
  "(HTTP response code was #{rcode})", rcode, rbody)
236
312
  end
237
313
 
238
- def self.handle_api_error(rcode, rbody)
314
+ def self.handle_api_error(resp)
239
315
  begin
240
- error_obj = JSON.parse(rbody)
316
+ error_obj = JSON.parse(resp.body)
241
317
  error_obj = Util.symbolize_names(error_obj)
242
- error = error_obj[:error] or raise PingppError.new # escape from parsing
318
+ error = error_obj[:error]
319
+ raise PingppError.new unless error && error.is_a?(Hash)
243
320
 
244
321
  rescue JSON::ParserError, PingppError
245
- raise general_api_error(rcode, rbody)
322
+ raise general_api_error(resp.code, resp.body)
246
323
  end
247
324
 
248
- case rcode
325
+ case resp.code
249
326
  when 400, 404
250
- raise invalid_request_error error, rcode, rbody, error_obj
327
+ raise invalid_request_error(error, resp, error_obj)
251
328
  when 401
252
- raise authentication_error error, rcode, rbody, error_obj
329
+ raise authentication_error(error, resp, error_obj)
253
330
  when 402
254
- raise channel_error error, rcode, rbody, error_obj
331
+ raise channel_error(error, resp, error_obj)
332
+ when 403
333
+ raise rate_limit_error(error, resp, error_obj)
255
334
  else
256
- raise api_error error, rcode, rbody, error_obj
335
+ raise api_error(error, resp, error_obj)
257
336
  end
258
337
 
259
338
  end
260
339
 
261
- def self.invalid_request_error(error, rcode, rbody, error_obj)
262
- InvalidRequestError.new(error[:message], error[:param], rcode,
263
- rbody, error_obj)
340
+ def self.invalid_request_error(error, resp, error_obj)
341
+ InvalidRequestError.new(error[:message], error[:param], resp.code,
342
+ resp.body, error_obj, resp.headers)
343
+ end
344
+
345
+ def self.authentication_error(error, resp, error_obj)
346
+ AuthenticationError.new(error[:message], resp.code, resp.body, error_obj,
347
+ resp.headers)
264
348
  end
265
349
 
266
- def self.authentication_error(error, rcode, rbody, error_obj)
267
- AuthenticationError.new(error[:message], rcode, rbody, error_obj)
350
+ def self.channel_error(error, resp, error_obj)
351
+ ChannelError.new(error[:message], error[:code], error[:param], resp.code,
352
+ resp.body, error_obj, resp.headers)
268
353
  end
269
354
 
270
- def self.channel_error(error, rcode, rbody, error_obj)
271
- ChannelError.new(error[:message], error[:code], error[:param], rcode, rbody, error_obj)
355
+ def self.rate_limit_error(error, resp, error_obj)
356
+ RateLimitError.new(error[:message], resp.code, resp.body, error_obj,
357
+ resp.headers)
272
358
  end
273
359
 
274
- def self.api_error(error, rcode, rbody, error_obj)
275
- APIError.new(error[:message], rcode, rbody, error_obj)
360
+ def self.api_error(error, resp, error_obj)
361
+ APIError.new(error[:message], resp.code, resp.body, error_obj, resp.headers)
276
362
  end
277
363
 
278
- def self.handle_restclient_error(e)
364
+ def self.handle_restclient_error(e, request_opts, retry_count, api_base_url=nil)
365
+ api_base_url = @api_base unless api_base_url
366
+
279
367
  connection_message = "Please check your internet connection and try again. " \
280
368
  "If this problem persists, you should check Pingpp's service status at " \
281
369
  "https://www.pingxx.com/status"
282
370
 
283
371
  case e
284
372
  when RestClient::RequestTimeout
285
- message = "Could not connect to Pingpp (#{@api_base}). #{connection_message}"
373
+ message = "Could not connect to Pingpp (#{api_base_url}). #{connection_message}"
286
374
 
287
375
  when RestClient::ServerBrokeConnection
288
- message = "The connection to the server (#{@api_base}) broke before the " \
376
+ message = "The connection to the server (#{api_base_url}) broke before the " \
289
377
  "request completed. #{connection_message}"
290
378
 
379
+ when OpenSSL::SSL::SSLError
380
+ message = "Could not establish a secure connection to Ping++, you may " \
381
+ "need to upgrade your OpenSSL version. To check, try running " \
382
+ "'openssl s_client -connect api.pingxx.com:443' from the " \
383
+ "command line."
384
+
291
385
  when RestClient::SSLCertificateNotVerified
292
386
  message = "Could not verify Pingpp's SSL certificate. " \
293
387
  "Please make sure that your network is not intercepting certificates. " \
294
- "(Try going to (#{@api_base}) in your browser.)"
388
+ "(Try going to (#{api_base_url}) in your browser.)"
295
389
 
296
390
  when SocketError
297
391
  message = "Unexpected error communicating when trying to connect to Pingpp. " \
@@ -303,6 +397,23 @@ module Pingpp
303
397
 
304
398
  end
305
399
 
400
+ if retry_count > 0
401
+ message += " Request was retried #{retry_count} times."
402
+ end
403
+
306
404
  raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
307
405
  end
406
+
407
+ def self.should_retry?(e, retry_count)
408
+ retry_count < self.max_network_retries &&
409
+ RETRY_EXCEPTIONS.any? { |klass| e.is_a?(klass) }
410
+ end
411
+
412
+ def self.sleep_time(retry_count)
413
+ sleep_seconds = [initial_network_retry_delay * (2 ** (retry_count - 1)), max_network_retry_delay].min
414
+ sleep_seconds = sleep_seconds * (0.5 * (1 + rand()))
415
+ sleep_seconds = [initial_network_retry_delay, sleep_seconds].max
416
+
417
+ sleep_seconds
418
+ end
308
419
  end
metadata CHANGED
@@ -1,36 +1,22 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pingpp
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.15
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xufeng Weng
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-08-18 00:00:00.000000000 Z
11
+ date: 2016-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.4'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.4'
27
- - !ruby/object:Gem::Dependency
28
- name: mime-types
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
32
18
  - !ruby/object:Gem::Version
33
- version: '1.25'
19
+ version: '1.4'
34
20
  - - "<"
35
21
  - !ruby/object:Gem::Version
36
22
  version: '4.0'
@@ -40,64 +26,10 @@ dependencies:
40
26
  requirements:
41
27
  - - ">="
42
28
  - !ruby/object:Gem::Version
43
- version: '1.25'
29
+ version: '1.4'
44
30
  - - "<"
45
31
  - !ruby/object:Gem::Version
46
32
  version: '4.0'
47
- - !ruby/object:Gem::Dependency
48
- name: json
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '1.8'
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- version: 1.8.1
57
- type: :runtime
58
- prerelease: false
59
- version_requirements: !ruby/object:Gem::Requirement
60
- requirements:
61
- - - "~>"
62
- - !ruby/object:Gem::Version
63
- version: '1.8'
64
- - - ">="
65
- - !ruby/object:Gem::Version
66
- version: 1.8.1
67
- - !ruby/object:Gem::Dependency
68
- name: mocha
69
- requirement: !ruby/object:Gem::Requirement
70
- requirements:
71
- - - "~>"
72
- - !ruby/object:Gem::Version
73
- version: 0.13.2
74
- type: :development
75
- prerelease: false
76
- version_requirements: !ruby/object:Gem::Requirement
77
- requirements:
78
- - - "~>"
79
- - !ruby/object:Gem::Version
80
- version: 0.13.2
81
- - !ruby/object:Gem::Dependency
82
- name: shoulda
83
- requirement: !ruby/object:Gem::Requirement
84
- requirements:
85
- - - "~>"
86
- - !ruby/object:Gem::Version
87
- version: '3.4'
88
- - - ">="
89
- - !ruby/object:Gem::Version
90
- version: 3.4.0
91
- type: :development
92
- prerelease: false
93
- version_requirements: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - "~>"
96
- - !ruby/object:Gem::Version
97
- version: '3.4'
98
- - - ">="
99
- - !ruby/object:Gem::Version
100
- version: 3.4.0
101
33
  description: PingPlusPlus is the easiest way to accept payments online. See https://pingxx.com
102
34
  for details.
103
35
  email:
@@ -111,15 +43,20 @@ files:
111
43
  - lib/pingpp/api_operations/create.rb
112
44
  - lib/pingpp/api_operations/delete.rb
113
45
  - lib/pingpp/api_operations/list.rb
46
+ - lib/pingpp/api_operations/request.rb
114
47
  - lib/pingpp/api_operations/update.rb
115
48
  - lib/pingpp/api_resource.rb
49
+ - lib/pingpp/batch_refund.rb
50
+ - lib/pingpp/batch_transfer.rb
116
51
  - lib/pingpp/charge.rb
52
+ - lib/pingpp/customs.rb
117
53
  - lib/pingpp/errors/api_connection_error.rb
118
54
  - lib/pingpp/errors/api_error.rb
119
55
  - lib/pingpp/errors/authentication_error.rb
120
56
  - lib/pingpp/errors/channel_error.rb
121
57
  - lib/pingpp/errors/invalid_request_error.rb
122
58
  - lib/pingpp/errors/pingpp_error.rb
59
+ - lib/pingpp/errors/rate_limit_error.rb
123
60
  - lib/pingpp/event.rb
124
61
  - lib/pingpp/identification.rb
125
62
  - lib/pingpp/list_object.rb
@@ -132,7 +69,7 @@ files:
132
69
  - lib/pingpp/version.rb
133
70
  - lib/pingpp/webhook.rb
134
71
  - lib/pingpp/wx_pub_oauth.rb
135
- homepage: https://pingxx.com/document/api
72
+ homepage: https://www.pingxx.com/api
136
73
  licenses:
137
74
  - MIT
138
75
  metadata: {}