alipay-easysdk-ruby 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Alipay
5
+ module EasySDK
6
+ module Kernel
7
+ module Util
8
+ class AES
9
+ def aes_encrypt(plain_text, key)
10
+ raise encryption_error(plain_text, key) if key.to_s.empty?
11
+
12
+ secret_key = Base64.decode64(key)
13
+ padded = add_pkcs7_padding(plain_text.to_s)
14
+
15
+ cipher = OpenSSL::Cipher.new('AES-128-CBC')
16
+ cipher.encrypt
17
+ cipher.key = secret_key
18
+ cipher.iv = "\0" * 16
19
+ cipher.padding = 0
20
+
21
+ encrypted = cipher.update(padded) + cipher.final
22
+ Base64.strict_encode64(encrypted)
23
+ rescue StandardError => e
24
+ raise StandardError, "AES加密失败,plainText=#{plain_text},keySize=#{key.to_s.length}。#{e.message}"
25
+ end
26
+
27
+ def aes_decrypt(cipher_text, key)
28
+ raise decryption_error(cipher_text, key) if key.to_s.empty?
29
+
30
+ secret_key = Base64.decode64(key)
31
+ encrypted = Base64.decode64(cipher_text)
32
+
33
+ decipher = OpenSSL::Cipher.new('AES-128-CBC')
34
+ decipher.decrypt
35
+ decipher.key = secret_key
36
+ decipher.iv = "\0" * 16
37
+ decipher.padding = 0
38
+
39
+ decrypted = decipher.update(encrypted) + decipher.final
40
+ strip_pkcs7_padding(decrypted)
41
+ rescue StandardError => e
42
+ raise StandardError, "AES解密失败,cipherText=#{cipher_text},keySize=#{key.to_s.length}。#{e.message}"
43
+ end
44
+
45
+ private
46
+
47
+ def add_pkcs7_padding(source)
48
+ str = source.to_s.strip
49
+ block = 16
50
+ pad = block - (str.bytesize % block)
51
+ pad = block if pad.zero?
52
+ str + (pad.chr * pad)
53
+ end
54
+
55
+ def strip_pkcs7_padding(source)
56
+ return source if source.nil? || source.empty?
57
+
58
+ char = source[-1]
59
+ num = char.ord
60
+ return source if num == 62
61
+ source[0...-num]
62
+ end
63
+
64
+ def encryption_error(plain_text, key)
65
+ StandardError.new("AES加密失败,plainText=#{plain_text},AES密钥为空。")
66
+ end
67
+
68
+ def decryption_error(cipher_text, key)
69
+ StandardError.new("AES解密失败,cipherText=#{cipher_text},AES密钥为空。")
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,63 @@
1
+ require 'openssl'
2
+ require 'digest/md5'
3
+
4
+ module Alipay
5
+ module EasySDK
6
+ module Kernel
7
+ module Util
8
+ class AntCertificationUtil
9
+ SUPPORTED_SIGNATURES = %w[sha1WithRSAEncryption sha256WithRSAEncryption].freeze
10
+
11
+ def cert_sn(cert_path)
12
+ cert = load_certificate(cert_path)
13
+ signature_digest(cert.issuer.to_a, cert.serial)
14
+ end
15
+
16
+ def public_key(cert_path)
17
+ cert = load_certificate(cert_path)
18
+ key_pem = cert.public_key.to_pem
19
+ key_pem.gsub(/-----BEGIN PUBLIC KEY-----/, '')
20
+ .gsub(/-----END PUBLIC KEY-----/, '')
21
+ .gsub(/\s+/, '')
22
+ .strip
23
+ end
24
+
25
+ def root_cert_sn(cert_path)
26
+ certs = load_cert_chain(cert_path)
27
+ sns = certs.each_with_object([]) do |cert, acc|
28
+ next unless SUPPORTED_SIGNATURES.include?(cert.signature_algorithm)
29
+
30
+ acc << signature_digest(cert.issuer.to_a, cert.serial)
31
+ end
32
+ sns.join('_')
33
+ end
34
+
35
+ private
36
+
37
+ def load_certificate(path)
38
+ raise ArgumentError, '证书路径不能为空' if path.nil? || path.to_s.strip.empty?
39
+
40
+ OpenSSL::X509::Certificate.new(File.read(path))
41
+ rescue Errno::ENOENT => e
42
+ raise RuntimeError, "无法读取证书文件: #{path} - #{e.message}"
43
+ end
44
+
45
+ def load_cert_chain(path)
46
+ pem = File.read(path)
47
+ pem.split(/(?=-----BEGIN CERTIFICATE-----)/)
48
+ .reject(&:empty?)
49
+ .map { |chunk| OpenSSL::X509::Certificate.new(chunk) }
50
+ rescue Errno::ENOENT => e
51
+ raise RuntimeError, "无法读取证书链文件: #{path} - #{e.message}"
52
+ end
53
+
54
+ def signature_digest(issuer_array, serial)
55
+ issuer_str = issuer_array.reverse.map { |name, data, _| "#{name}=#{data}" }.join(',')
56
+ serial_str = serial.to_i.to_s
57
+ Digest::MD5.hexdigest("#{issuer_str}#{serial_str}")
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -7,14 +7,9 @@ module Alipay
7
7
  class JsonUtil
8
8
  # 完全复制PHP版本的toJsonString方法
9
9
  def to_json_string(obj)
10
- case obj
11
- when Hash, Array
12
- JSON.generate(obj)
13
- when String
14
- obj
15
- else
16
- obj.to_s
17
- end
10
+ return {} if obj.nil?
11
+ raise ArgumentError, 'to_json_string expects a Hash' unless obj.is_a?(Hash)
12
+ convert_map(obj)
18
13
  end
19
14
 
20
15
  def self.from_json_string(json_string)
@@ -23,6 +18,53 @@ module Alipay
23
18
  rescue JSON::ParserError
24
19
  json_string
25
20
  end
21
+
22
+ private
23
+
24
+ def convert_map(hash)
25
+ return hash unless hash.is_a?(Hash)
26
+
27
+ hash.each_with_object({}) do |(key, value), result|
28
+ result[underscore_key(key)] = convert_value(value)
29
+ end
30
+ end
31
+
32
+ def convert_value(value)
33
+ case value
34
+ when Hash
35
+ convert_map(value)
36
+ when Array
37
+ value.map { |item| convert_value(item) }
38
+ else
39
+ convert_object(value)
40
+ end
41
+ end
42
+
43
+ def convert_object(value)
44
+ return value if primitive?(value)
45
+
46
+ if value.respond_to?(:to_h)
47
+ convert_map(value.to_h)
48
+ elsif value.respond_to?(:to_hash)
49
+ convert_map(value.to_hash)
50
+ else
51
+ value
52
+ end
53
+ end
54
+
55
+ def underscore_key(key)
56
+ str = key.to_s
57
+ underscored = str.gsub(/([A-Z]+)/) { "_#{$1.downcase}" }
58
+ underscored.gsub(/__+/, '_').sub(/^_/, '')
59
+ end
60
+
61
+ def primitive?(value)
62
+ value.is_a?(String) ||
63
+ value.is_a?(Numeric) ||
64
+ value == true ||
65
+ value == false ||
66
+ value.nil?
67
+ end
26
68
  end
27
69
  end
28
70
  end
@@ -0,0 +1,34 @@
1
+ module Alipay
2
+ module EasySDK
3
+ module Kernel
4
+ module Util
5
+ class PageUtil
6
+ def build_form(action_url, parameters)
7
+ charset = Alipay::EasySDK::Kernel::AlipayConstants::DEFAULT_CHARSET
8
+ form = "<form id='alipaysubmit' name='alipaysubmit' action='#{action_url}?charset=#{charset}' method='POST'>"
9
+ params_enum(parameters).each do |key, val|
10
+ next if check_empty(val)
11
+
12
+ escaped = val.to_s.gsub("'", "&apos;")
13
+ form += "<input type='hidden' name='#{key}' value='#{escaped}'/>"
14
+ end
15
+ form += "<input type='submit' value='ok' style='display:none;'></form>"
16
+ form += "<script>document.forms['alipaysubmit'].submit();</script>"
17
+ form
18
+ end
19
+
20
+ private
21
+
22
+ def params_enum(parameters)
23
+ return [] if parameters.nil?
24
+ parameters.to_a
25
+ end
26
+
27
+ def check_empty(value)
28
+ !value || value.to_s.strip == ''
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -7,7 +7,12 @@ module Alipay
7
7
  return true if response.respond_to?(:code) && response.code.to_s == '10000'
8
8
 
9
9
  code_blank = !response.respond_to?(:code) || blank?(response.code)
10
- sub_code_blank = !response.respond_to?(:sub_code) || blank?(response.sub_code)
10
+ sub_code_value = if response.respond_to?(:subCode)
11
+ response.subCode
12
+ elsif response.respond_to?(:sub_code)
13
+ response.sub_code
14
+ end
15
+ sub_code_blank = blank?(sub_code_value)
11
16
 
12
17
  code_blank && sub_code_blank
13
18
  end
@@ -49,7 +49,7 @@ module Alipay
49
49
  # 调用openssl内置方法验签,完全模仿PHP的openssl_verify
50
50
  public_key = OpenSSL::PKey::RSA.new(res)
51
51
  digest = OpenSSL::Digest::SHA256.new
52
- decoded_sign = Base64.decode64(sign)
52
+ decoded_sign = Base64.decode64(sign.to_s)
53
53
 
54
54
  result = public_key.verify(digest, decoded_sign, content)
55
55
  return result
@@ -59,14 +59,16 @@ module Alipay
59
59
  end
60
60
 
61
61
  def verify_params(parameters, public_key)
62
- sign = parameters['sign']
63
- content = get_sign_content(parameters)
64
- return verify(content, sign, public_key)
62
+ normalized = stringify_keys(parameters)
63
+ sign = normalized['sign'] || ''
64
+ content = get_sign_content(normalized)
65
+ verify(content, sign, public_key)
65
66
  end
66
67
 
67
68
  def get_sign_content(params)
69
+ normalized = stringify_keys(params)
68
70
  # 模拟PHP的ksort
69
- sorted_params = params.sort_by { |k, _| k.to_s }.to_h
71
+ sorted_params = normalized.sort.to_h
70
72
 
71
73
  # 移除sign和sign_type字段
72
74
  sorted_params.delete('sign')
@@ -89,6 +91,14 @@ module Alipay
89
91
 
90
92
  private
91
93
 
94
+ def stringify_keys(hash)
95
+ return {} if hash.nil?
96
+
97
+ hash.each_with_object({}) do |(key, value), acc|
98
+ acc[key.to_s] = value
99
+ end
100
+ end
101
+
92
102
  # 完全复制PHP的wordwrap函数
93
103
  def wordwrap(str, width = 75, break_str = "\n", cut = false)
94
104
  if str.nil? || str.empty?
@@ -0,0 +1,75 @@
1
+ require_relative '../../kernel/easy_sdk_kernel'
2
+ require_relative 'models/alipay_trade_app_pay_response'
3
+
4
+ module Alipay
5
+ module EasySDK
6
+ module Payment
7
+ module App
8
+ class Client
9
+ def initialize(kernel)
10
+ @kernel = kernel
11
+ end
12
+
13
+ def pay(subject, out_trade_no, total_amount)
14
+ system_params = {
15
+ 'method' => 'alipay.trade.app.pay',
16
+ 'app_id' => @kernel.get_config('appId'),
17
+ 'timestamp' => @kernel.get_timestamp,
18
+ 'format' => 'json',
19
+ 'version' => '1.0',
20
+ 'alipay_sdk' => @kernel.get_sdk_version,
21
+ 'charset' => 'UTF-8',
22
+ 'sign_type' => @kernel.get_config('signType'),
23
+ 'app_cert_sn' => @kernel.get_merchant_cert_sn,
24
+ 'alipay_root_cert_sn' => @kernel.get_alipay_root_cert_sn
25
+ }
26
+ biz_params = {
27
+ 'subject' => subject,
28
+ 'out_trade_no' => out_trade_no,
29
+ 'total_amount' => total_amount
30
+ }
31
+ text_params = {}
32
+
33
+ sign = @kernel.sign(system_params, biz_params, text_params, @kernel.get_config('merchantPrivateKey'))
34
+ response = {
35
+ Alipay::EasySDK::Kernel::AlipayConstants::BODY_FIELD => @kernel.generate_order_string(system_params, biz_params, text_params, sign)
36
+ }
37
+ Models::AlipayTradeAppPayResponse.from_map(response)
38
+ end
39
+
40
+ def agent(app_auth_token)
41
+ @kernel.inject_text_param('app_auth_token', app_auth_token)
42
+ self
43
+ end
44
+
45
+ def auth(auth_token)
46
+ @kernel.inject_text_param('auth_token', auth_token)
47
+ self
48
+ end
49
+
50
+ def async_notify(url)
51
+ @kernel.inject_text_param('notify_url', url)
52
+ self
53
+ end
54
+
55
+ def route(test_url)
56
+ @kernel.inject_text_param('ws_service_url', test_url)
57
+ self
58
+ end
59
+
60
+ def optional(key, value)
61
+ @kernel.inject_biz_param(key, value)
62
+ self
63
+ end
64
+
65
+ def batch_optional(optional_args)
66
+ optional_args.each do |opt_key, opt_value|
67
+ @kernel.inject_biz_param(opt_key, opt_value)
68
+ end
69
+ self
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,19 @@
1
+ module Alipay
2
+ module EasySDK
3
+ module Payment
4
+ module App
5
+ module Models
6
+ class AlipayTradeAppPayResponse
7
+ attr_accessor :body
8
+
9
+ def self.from_map(response)
10
+ new.tap do |instance|
11
+ instance.body = response[Alipay::EasySDK::Kernel::AlipayConstants::BODY_FIELD]
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,6 +1,7 @@
1
1
  require 'net/http'
2
2
  require 'uri'
3
3
  require 'cgi'
4
+ require 'openssl'
4
5
 
5
6
  require_relative '../../kernel/easy_sdk_kernel'
6
7
  require_relative 'models/alipay_trade_create_response'
@@ -16,6 +17,27 @@ module Alipay
16
17
  module Payment
17
18
  module Common
18
19
  class Client
20
+ class TeaError < StandardError
21
+ attr_reader :data, :code
22
+
23
+ def initialize(data = {}, message = '', code = nil, cause = nil)
24
+ super(message)
25
+ @data = data
26
+ @code = code
27
+ set_backtrace(cause.backtrace) if cause
28
+ end
29
+ end
30
+
31
+ class TeaUnableRetryError < StandardError
32
+ attr_reader :last_request, :last_exception
33
+
34
+ def initialize(last_request, last_exception)
35
+ super(last_exception&.message)
36
+ @last_request = last_request
37
+ @last_exception = last_exception
38
+ end
39
+ end
40
+
19
41
  def initialize(kernel)
20
42
  @kernel = kernel
21
43
  end
@@ -71,7 +93,7 @@ module Alipay
71
93
 
72
94
  def verify_notify(parameters)
73
95
  if @kernel.is_cert_mode
74
- @kernel.verify_params(parameters, @kernel.extract_alipay_public_key(@kernel.get_alipay_cert_sn('')))
96
+ @kernel.verify_params(parameters, @kernel.extract_alipay_public_key(''))
75
97
  else
76
98
  @kernel.verify_params(parameters, @kernel.get_config('alipayPublicKey'))
77
99
  end
@@ -117,7 +139,7 @@ module Alipay
117
139
  sign = @kernel.sign(system_params, biz_params, text_params, @kernel.get_config('merchantPrivateKey'))
118
140
 
119
141
  text_query = (@kernel.text_params || {}).dup
120
- query_params = { 'sign' => sign }.merge(system_params)
142
+ query_params = { Alipay::EasySDK::Kernel::AlipayConstants::SIGN_FIELD => sign }.merge(system_params)
121
143
  query_params.merge!(text_query) unless text_query.empty?
122
144
 
123
145
  uri = build_gateway_uri(query_params)
@@ -130,11 +152,15 @@ module Alipay
130
152
  resp_map = @kernel.read_as_json(response, api_method)
131
153
 
132
154
  unless verify_response(resp_map)
133
- raise '验签失败,请检查支付宝公钥设置是否正确。'
155
+ raise TeaError.new({}, '验签失败,请检查支付宝公钥设置是否正确。')
134
156
  end
135
157
 
136
158
  map = @kernel.to_resp_model(resp_map)
137
159
  response_class.from_map(map)
160
+ rescue TeaError => e
161
+ raise e
162
+ rescue StandardError => e
163
+ raise TeaError.new({}, e.message, nil, e)
138
164
  ensure
139
165
  clear_optional_params
140
166
  end
@@ -155,17 +181,17 @@ module Alipay
155
181
  end
156
182
 
157
183
  def build_gateway_uri(query_params)
158
- host = @kernel.get_config('gatewayHost').to_s.sub(%r{/gateway\.do$}, '')
159
- scheme = @kernel.get_config('protocol') || 'https'
184
+ host = @kernel.get_config(Alipay::EasySDK::Kernel::AlipayConstants::HOST_CONFIG_KEY).to_s.sub(%r{/gateway\.do$}, '')
185
+ scheme = @kernel.get_config(Alipay::EasySDK::Kernel::AlipayConstants::PROTOCOL_CONFIG_KEY) || 'https'
160
186
  uri = URI("#{scheme}://#{host}/gateway.do")
161
187
  uri.query = URI.encode_www_form(query_params.sort.to_h)
162
188
  uri
163
189
  end
164
190
 
165
191
  def perform_http_request(uri, request)
166
- http = Net::HTTP.new(uri.host, uri.port)
192
+ http = build_http_client(uri)
167
193
  http.use_ssl = uri.scheme == 'https'
168
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl?
194
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl? && ignore_ssl?
169
195
  http.open_timeout = 15
170
196
  http.read_timeout = 15
171
197
  http.request(request)
@@ -183,6 +209,32 @@ module Alipay
183
209
  @kernel.optional_text_params.clear if @kernel.optional_text_params
184
210
  @kernel.optional_biz_params.clear if @kernel.optional_biz_params
185
211
  end
212
+
213
+ def build_http_client(uri)
214
+ proxy_settings = http_proxy_settings
215
+ if proxy_settings
216
+ Net::HTTP.new(uri.host, uri.port, *proxy_settings)
217
+ else
218
+ Net::HTTP.new(uri.host, uri.port)
219
+ end
220
+ end
221
+
222
+ def http_proxy_settings
223
+ raw_proxy = @kernel.get_config('httpProxy')
224
+ return nil if raw_proxy.nil? || raw_proxy.to_s.strip.empty?
225
+
226
+ proxy_uri = raw_proxy =~ %r{^https?://} ? URI(raw_proxy) : URI("http://#{raw_proxy}")
227
+ [proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password]
228
+ rescue URI::InvalidURIError
229
+ nil
230
+ end
231
+
232
+ def ignore_ssl?
233
+ value = @kernel.get_config('ignoreSSL')
234
+ return false if value.nil?
235
+
236
+ value == true || value.to_s.strip.downcase == 'true'
237
+ end
186
238
  end
187
239
  end
188
240
  end
@@ -20,20 +20,6 @@ module Alipay
20
20
  end
21
21
  end
22
22
  end
23
-
24
- def success?
25
- respond_to?('code') ? public_send('code') == '10000' : false
26
- end
27
-
28
- def error_message
29
- if respond_to?('sub_msg') && public_send('sub_msg')
30
- public_send('sub_msg')
31
- elsif respond_to?('msg')
32
- public_send('msg')
33
- else
34
- nil
35
- end
36
- end
37
23
  end
38
24
  end
39
25
  end
@@ -36,7 +36,7 @@ module Alipay
36
36
  sign = @kernel.sign(system_params, biz_params, text_params, @kernel.get_config('merchantPrivateKey'))
37
37
 
38
38
  response = {
39
- 'body' => @kernel.generate_page('POST', system_params, biz_params, text_params, sign),
39
+ Alipay::EasySDK::Kernel::AlipayConstants::BODY_FIELD => @kernel.generate_page('POST', system_params, biz_params, text_params, sign),
40
40
  'payment_url' => @kernel.generate_payment_url(system_params, biz_params, text_params, sign)
41
41
  }
42
42
 
@@ -8,26 +8,10 @@ module Alipay
8
8
 
9
9
  def self.from_map(response)
10
10
  new.tap do |instance|
11
- instance.body = response['body']
11
+ instance.body = response[Alipay::EasySDK::Kernel::AlipayConstants::BODY_FIELD]
12
12
  instance.payment_url = response['payment_url']
13
13
  end
14
14
  end
15
-
16
- def success?
17
- !body.nil? && !body.to_s.empty?
18
- end
19
-
20
- def form
21
- body
22
- end
23
-
24
- def error_message
25
- nil
26
- end
27
-
28
- def to_s
29
- body || ''
30
- end
31
15
  end
32
16
  end
33
17
  end
@@ -39,7 +39,7 @@ module Alipay
39
39
  sign = @kernel.sign(system_params, biz_params, text_params, @kernel.get_config("merchantPrivateKey"))
40
40
 
41
41
  response = {
42
- "body" => @kernel.generate_page("POST", system_params, biz_params, text_params, sign),
42
+ Alipay::EasySDK::Kernel::AlipayConstants::BODY_FIELD => @kernel.generate_page("POST", system_params, biz_params, text_params, sign),
43
43
  "payment_url" => @kernel.generate_payment_url(system_params, biz_params, text_params, sign)
44
44
  }
45
45
  return Alipay::EasySDK::Payment::Wap::Models::AlipayTradeWapPayResponse.from_map(response)
@@ -8,26 +8,10 @@ module Alipay
8
8
 
9
9
  def self.from_map(response)
10
10
  new.tap do |instance|
11
- instance.body = response['body']
11
+ instance.body = response[Alipay::EasySDK::Kernel::AlipayConstants::BODY_FIELD]
12
12
  instance.payment_url = response['payment_url']
13
13
  end
14
14
  end
15
-
16
- def success?
17
- !body.nil? && !body.to_s.empty?
18
- end
19
-
20
- def form
21
- body
22
- end
23
-
24
- def error_message
25
- nil
26
- end
27
-
28
- def to_s
29
- body || ''
30
- end
31
15
  end
32
16
  end
33
17
  end
@@ -1,5 +1,5 @@
1
1
  module Alipay
2
2
  module EasySDK
3
- VERSION = '1.0.1'
3
+ VERSION = '1.0.3'
4
4
  end
5
5
  end
@@ -17,6 +17,10 @@ module Alipay
17
17
  Kernel::Factory.payment
18
18
  end
19
19
 
20
+ def app
21
+ payment.app
22
+ end
23
+
20
24
  def wap
21
25
  payment.wap
22
26
  end