wechatpay-api 0.0.2 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c02376e1f66aced738a92265233ed65a159fa677672498d098d406fd6f68ebeb
4
- data.tar.gz: d996e6f70e9a473a8a7d05a2f32730e895a55e5388e873d3db098fab9f5a0216
3
+ metadata.gz: b97b7c1f8ed9f448edd975a6adf2bf554840d5442fbc3dc5928be733941fdcd2
4
+ data.tar.gz: 39723a0aa3661c5bff6a1ebed35c67a79c0b1b976354882aa0651bdc2189ac89
5
5
  SHA512:
6
- metadata.gz: d00c99a38872e1eae30fd765c94aeb954f0066b38fd179247a363b8f2af2756dd90775a86d0b0ecf7f3e3532012824d24f7e5daddf3ff526fb1cb8c4398d0879
7
- data.tar.gz: ad86f1ba1f248ce12f8664643c616601b7842b4601108512d58b95ab3f62d04e5f3e9b8d195daeeb5207a68bc6f1baa05da7b11ca107f7e03c7857128c15d5b3
6
+ metadata.gz: 3f7be18010ec031f42fc57978b9adb186c1adf876e9c29a76a574a8c137f4d70fe99ae9862a791c4bee22223e5aae13ee1d9bd279e0f8968224c128f3f33252e
7
+ data.tar.gz: 94d67a3a5483c82c26000f9064c22f46234e8c183f1d5318c6c5a77263151caa7e211780941a09c8dc051de25abad59c8c72cdca550b18205d1943859a4899f9
@@ -1,23 +1,19 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- wechatpay-api (0.0.1)
4
+ wechatpay-api (0.0.2)
5
5
  faraday (~> 1.0)
6
- gyoku (>= 1.0.0)
7
6
  multi_json (~> 1.0)
8
- nokogiri (~> 1.0)
9
- nori (~> 2.0)
10
7
 
11
8
  GEM
12
9
  remote: https://rubygems.org/
13
10
  specs:
14
11
  addressable (2.7.0)
15
12
  public_suffix (>= 2.0.2, < 5.0)
16
- builder (3.2.4)
17
13
  coderay (1.1.3)
18
14
  crack (0.4.4)
19
15
  diff-lcs (1.4.4)
20
- faraday (1.1.0)
16
+ faraday (1.2.0)
21
17
  multipart-post (>= 1.2, < 3)
22
18
  ruby2_keywords
23
19
  ffi (1.13.1)
@@ -36,21 +32,15 @@ GEM
36
32
  guard (~> 2.1)
37
33
  guard-compat (~> 1.1)
38
34
  rspec (>= 2.99.0, < 4.0)
39
- gyoku (1.3.1)
40
- builder (>= 2.1.2)
41
35
  hashdiff (1.0.1)
42
36
  listen (3.3.3)
43
37
  rb-fsevent (~> 0.10, >= 0.10.3)
44
38
  rb-inotify (~> 0.9, >= 0.9.10)
45
39
  lumberjack (1.2.8)
46
40
  method_source (1.0.0)
47
- mini_portile2 (2.4.0)
48
41
  multi_json (1.15.0)
49
42
  multipart-post (2.1.1)
50
43
  nenv (0.3.0)
51
- nokogiri (1.10.10)
52
- mini_portile2 (~> 2.4.0)
53
- nori (2.6.0)
54
44
  notiffany (0.1.3)
55
45
  nenv (~> 0.1)
56
46
  shellany (~> 0.0)
data/README.md CHANGED
@@ -1,2 +1,71 @@
1
1
  # wechatpay-api
2
- Yet Another wechatpay sdk for ruby 另一个微信支付Ruby SDK。基于API V3.0
2
+ Yet Another wechatpay sdk for ruby 另一个微信支付Ruby SDK
3
+ APIv3 接口版本V3
4
+
5
+ 公众号API SDK: https://github.com/lazing/wechat-api
6
+
7
+ # Install
8
+
9
+ ```ruby
10
+ gem 'wechatpay-api', '~>0.1'
11
+ ```
12
+
13
+
14
+ or
15
+
16
+ ```ruby
17
+ gem install wechatpay-api
18
+ ```
19
+
20
+ # Usage
21
+
22
+ ## Prepare
23
+
24
+ Essential configure items and datus from wechat pay 需要从微信支付获取的必要配置项目
25
+
26
+ * appid - 公众号ID
27
+ * mchid - 商户号
28
+ * key - 32 bytes secret key to encrypt and decrypt 32字节加解密key
29
+ * cert_no - APIv3 certification serial no. 从微信支付获取的APIv3证书序列号
30
+ * cert - APIv3 certification private key pem, content as string. 从微信支付获取的APIv3证书私钥,内容作为文本输入
31
+
32
+ ## Configuration and direct usage
33
+
34
+ ```ruby
35
+ # initialize with a file like wechatpay-api.rb inside rails config initilizers
36
+ Wechatpay::Api.client do |klass|
37
+ klass.new 'appid', 'mchid', key: 'key', cert: 'apiv3 certification...', cert_no: 'cert_no'
38
+ end
39
+
40
+ # next in anywhere needs, using get or post, will sign automatic. response body
41
+ response_body_as_hash_symbolized = Wechatpay::Api.client.get '/path', params
42
+ response_body_as_hash_symbolized = Wechatpay::Api.client.post '/path', hash_as_body, headers
43
+
44
+ # check incoming request
45
+ Wechatpay::Api.client.verify headers, body
46
+ ```
47
+
48
+ ## JSAPI Helper Example
49
+
50
+ ```ruby
51
+ # initilized somehow Wechatpay::Api.client do |klass| ..... end
52
+ # reference the client
53
+ client = Wechatpay::Api.client
54
+ # STEP 1. after generate your own order object, start prepay
55
+ prepay_id = client.js_prepay(openid, out_trade_no, description, total_amount, **opts)
56
+
57
+ # STEP 2. get signature for WXJS SDK call
58
+ timestamp = Time.now.to_i
59
+ nonce = SecureRandom.hex
60
+ package = "prepay_id=#{prepay_id}"
61
+ signature = client.js_sign(package, timestamp, nonce)
62
+
63
+ # STEP 3. expose to view and start payment
64
+
65
+ # STEP 4. build a API endpoint to receive wechat payment notice
66
+ # request header and body
67
+
68
+ decrypted_hash = client.notice(headers, body)
69
+
70
+ ```
71
+
@@ -0,0 +1 @@
1
+ require 'wechatpay/api'
@@ -1,16 +1,15 @@
1
1
  module Wechatpay
2
2
  module Api
3
3
  class Error < StandardError; end
4
- module V2
5
- end
4
+ module V3; end
6
5
 
7
6
  def self.client(appid = 'origin_id')
8
7
  var = "@v#{appid}"
9
- if instance_variable_defined?(var)
10
- instance_variable_get(var)
11
- elsif block_given?
12
- c = yield(V2::Client)
8
+ if block_given?
9
+ c = yield(V3::Client)
13
10
  instance_variable_set var, c
11
+ elsif instance_variable_defined?(var)
12
+ instance_variable_get(var)
14
13
  else
15
14
  raise Error, :not_initialized
16
15
  end
@@ -18,4 +17,7 @@ module Wechatpay
18
17
  end
19
18
  end
20
19
 
21
- require 'wechatpay/api/v2/client'
20
+ require 'wechatpay/api/v3/client'
21
+ require 'wechatpay/api/v3/trade'
22
+ require 'wechatpay/api/v3/jsapi'
23
+ require 'wechatpay/api/v3/cert'
@@ -0,0 +1,33 @@
1
+ require 'multi_json'
2
+ require 'singleton'
3
+ require 'openssl'
4
+
5
+ module Wechatpay
6
+ module Api
7
+ module V3
8
+ class Cert
9
+ include Singleton
10
+
11
+ attr_accessor :expires_at, :serial_no, :cert, :certificate
12
+
13
+ def update(json)
14
+ Wechatpay::Api.client.logger.debug { "Cert JSON: #{json}" }
15
+ expire_time = DateTime.parse(json[:expire_time])
16
+ return unless expires_at.nil? || expire_time > expires_at
17
+
18
+ @serial_no = json[:serial_no]
19
+ @expires_at = expire_time
20
+ ec = json[:encrypt_certificate]
21
+ @cert = yield(ec[:ciphertext], ec[:nonce], ec[:associated_data])
22
+ @certificate = OpenSSL::X509::Certificate.new @cert
23
+ end
24
+
25
+ def load
26
+ return false if cert.nil? || expires_at < DateTime.now
27
+
28
+ self
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,132 @@
1
+ require 'base64'
2
+ require 'faraday'
3
+ require 'multi_json'
4
+ require 'openssl'
5
+ require 'logger'
6
+ require 'securerandom'
7
+
8
+ module Wechatpay
9
+ module Api
10
+ module V3
11
+ class Client
12
+
13
+ attr_reader :rsa_key, :serial_no, :mch_id, :appid, :key
14
+ attr_accessor :logger, :site
15
+
16
+ SCHEMA = 'WECHATPAY2-SHA256-RSA2048'.freeze
17
+
18
+ def initialize(appid, mch_id, **opts)
19
+ @appid = appid
20
+ @mch_id = mch_id
21
+ @rsa_key = OpenSSL::PKey::RSA.new opts[:cert] if opts[:cert]
22
+ @serial_no = opts[:cert_no]
23
+ @key = opts[:key]
24
+ @site = opts[:site] || 'https://api.mch.weixin.qq.com'
25
+
26
+ @logger = Logger.new(STDOUT)
27
+ end
28
+
29
+ def connection
30
+ Faraday.new(url: @site) do |conn|
31
+ conn.request :retry
32
+ conn.response :logger
33
+ # conn.response :raise_error
34
+ conn.adapter :net_http
35
+ end
36
+ end
37
+
38
+ def get(path, params = nil)
39
+ resp = connection.get(path, params) do |req|
40
+ path = req.path
41
+ path = [req.path, Faraday::Utils.build_query(req.params)].join('?') unless params.nil? || params.empty?
42
+ req.headers['Authorization'] = authorization_header('GET', path, nil)
43
+ req.headers['Accept'] = 'application/json'
44
+ end
45
+ handle resp
46
+ end
47
+
48
+ def post(path, data, **headers)
49
+ body = data.is_a?(Hash) ? MultiJson.dump(data) : data
50
+ resp = connection.post(path, body, headers) do |req|
51
+ req.headers['Authorization'] = authorization_header('POST', path, body)
52
+ req.headers['Accept'] = 'application/json'
53
+ end
54
+ handle resp
55
+ end
56
+
57
+ def verify(headers, body)
58
+ sha256 = OpenSSL::Digest::SHA256.new
59
+ key = cert.certificate.public_key
60
+ sign = Base64.strict_decode64(headers['Wechatpay-Signature'])
61
+ data = %w[Wechatpay-Timestamp Wechatpay-Nonce].map { |k| headers[k] }
62
+ key.verify sha256, sign, data.append(body).join("\n") + "\n"
63
+ end
64
+
65
+ def cert
66
+ Wechatpay::Api::V3::Cert.instance.load || update_certs
67
+ end
68
+
69
+ def update_certs
70
+ certs = get('/v3/certificates')[:data]
71
+ certs.map do |raw|
72
+ Wechatpay::Api::V3::Cert.instance.update(raw, &method(:decrypt))
73
+ end
74
+ Wechatpay::Api::V3::Cert.instance
75
+ end
76
+
77
+ def handle(resp)
78
+ logger.debug { "HANDLE RESPONSE: #{resp.inspect}" }
79
+ data = resp.body
80
+ raise :empty_body unless data && !data.empty?
81
+
82
+ MultiJson.load data, symbolize_keys: true
83
+ end
84
+
85
+ def sign_with(body, method, path, timestamp, rnd)
86
+ str = [method, path, timestamp, rnd, body].join("\n") + "\n"
87
+ logger.debug { "Sign Content: #{str.inspect}" }
88
+ sign_content(str)
89
+ end
90
+
91
+ def sign_content(content)
92
+ digest = OpenSSL::Digest::SHA256.new
93
+ signed = rsa_key.sign(digest, content)
94
+ Base64.strict_encode64 signed
95
+ end
96
+
97
+ def authorization_header(http_method, path, body)
98
+ [SCHEMA, authorization_params(http_method, path, body)].join ' '
99
+ end
100
+
101
+ def authorization_params(http_method, path, body)
102
+ timestamp = Time.now.to_i
103
+ rnd = SecureRandom.hex
104
+ signature = sign_with(body, http_method, path, timestamp, rnd)
105
+ [
106
+ "mchid=\"#{mch_id}\"", "serial_no=\"#{serial_no}\"", "nonce_str=\"#{rnd}\"",
107
+ "timestamp=\"#{timestamp}\"", "signature=\"#{signature}\""
108
+ ].join(',')
109
+ end
110
+
111
+ def decrypt(cipher_text, nonce, auth_data)
112
+ raise :cipher_text_invalid if (cipher_text.try(:length) || 0) < 16
113
+
114
+ data = Base64.strict_decode64(cipher_text)
115
+ dec = dechipher(data, nonce, auth_data)
116
+ dec.update(data[0, data.length - 16]).tap { |s| logger.debug "DEC: #{s}" } + dec.final
117
+ end
118
+
119
+ def dechipher(data, nonce, auth_data)
120
+ chipher = OpenSSL::Cipher.new 'aes-256-gcm'
121
+ chipher.decrypt
122
+ chipher.key = key
123
+ chipher.iv = nonce
124
+ chipher.padding = 0
125
+ chipher.auth_data = auth_data
126
+ chipher.auth_tag = data[-16, 16]
127
+ chipher
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,26 @@
1
+
2
+ module Wechatpay
3
+ module Api
4
+ module V3
5
+ module JSAPI
6
+ def js_prepay(openid, out_trade_no, description, total_amount, **opts)
7
+ data = {
8
+ appid: appid, mchid: mch_id, description: description,
9
+ out_trade_no: out_trade_no, amount: { total: total_amount, currency: 'CNY' },
10
+ payer: { openid: openid }
11
+ }.merge(opts)
12
+ res = post '/v3/pay/transactions/jsapi', data
13
+ res[:prepay_id]
14
+ end
15
+
16
+ def js_sign(package, timestamp, nonce)
17
+ str = [appid, timestamp, nonce, package].join("\n") + "\n"
18
+ sign_content(str)
19
+ end
20
+ end
21
+
22
+ Client.include JSAPI
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,22 @@
1
+ require 'multi_json'
2
+
3
+ module Wechatpay
4
+ module Api
5
+ module V3
6
+ module Trade
7
+
8
+ def notice(headers, payload)
9
+ raise :verify_fail unless verify(headers, payload)
10
+
11
+ data = MultiJson.load(payload, symbolize_keys: true)
12
+ r = data[:resource]
13
+ text = decrypt r[:ciphertext], r[:nonce], r[:associated_data]
14
+ MultiJson.load(text, symbolize_keys: true)
15
+ end
16
+ end
17
+
18
+ Client.include Trade
19
+ end
20
+ end
21
+ end
22
+
@@ -1,5 +1,5 @@
1
1
  module Wechatpay
2
2
  module Api
3
- VERSION = '0.0.2'
3
+ VERSION = '0.2.1'
4
4
  end
5
5
  end
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
- $:.unshift File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'wechatpay/api/version'
5
6
 
6
7
  Gem::Specification.new do |s|
@@ -18,10 +19,7 @@ Gem::Specification.new do |s|
18
19
  s.require_paths = ['lib']
19
20
 
20
21
  s.add_dependency 'faraday', '~> 1.0'
21
- s.add_dependency 'gyoku', '>= 1.0.0'
22
22
  s.add_dependency 'multi_json', '~> 1.0'
23
- s.add_dependency 'nokogiri', '~> 1.0'
24
- s.add_dependency 'nori', '~> 2.0'
25
23
 
26
24
  s.add_development_dependency 'bundler', '~> 1.0'
27
25
  s.add_development_dependency 'guard', '~> 2.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wechatpay-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - lazing
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-15 00:00:00.000000000 Z
11
+ date: 2021-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
- - !ruby/object:Gem::Dependency
28
- name: gyoku
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 1.0.0
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: 1.0.0
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: multi_json
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,34 +38,6 @@ dependencies:
52
38
  - - "~>"
53
39
  - !ruby/object:Gem::Version
54
40
  version: '1.0'
55
- - !ruby/object:Gem::Dependency
56
- name: nokogiri
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '1.0'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '1.0'
69
- - !ruby/object:Gem::Dependency
70
- name: nori
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '2.0'
76
- type: :runtime
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '2.0'
83
41
  - !ruby/object:Gem::Dependency
84
42
  name: bundler
85
43
  requirement: !ruby/object:Gem::Requirement
@@ -190,9 +148,12 @@ files:
190
148
  - Guardfile
191
149
  - LICENSE.md
192
150
  - README.md
151
+ - lib/wechatpay-api.rb
193
152
  - lib/wechatpay/api.rb
194
- - lib/wechatpay/api/v2/client.rb
195
- - lib/wechatpay/api/v2/pay.rb
153
+ - lib/wechatpay/api/v3/cert.rb
154
+ - lib/wechatpay/api/v3/client.rb
155
+ - lib/wechatpay/api/v3/jsapi.rb
156
+ - lib/wechatpay/api/v3/trade.rb
196
157
  - lib/wechatpay/api/version.rb
197
158
  - wechatpay-api.gemspec
198
159
  homepage: https://github.com/lazing/wechatpay-api
@@ -1,118 +0,0 @@
1
- require 'faraday'
2
- require 'multi_json'
3
- require 'openssl'
4
- require 'logger'
5
- require 'nokogiri'
6
- require 'nori'
7
- require 'gyoku'
8
- require 'securerandom'
9
-
10
- require 'wechatpay/api/v2/pay'
11
-
12
- module Wechatpay
13
- module Api
14
- module V2
15
- class Client
16
-
17
- include Pay
18
-
19
- attr_reader :appid, :mch_id, :key
20
- attr_accessor :logger, :site
21
-
22
- def initialize(appid, mch_id, **opts)
23
- @appid = appid
24
- @mch_id = mch_id
25
- @key = opts[:key]
26
- @site = opts[:site] || 'https://api.mch.weixin.qq.com'
27
-
28
- @logger = Logger.new(STDOUT)
29
- end
30
-
31
- def post(url, params, headers = {})
32
- data = sign(merge(params))
33
- response = connection.post url, xml(data), headers
34
- logger.debug { { response: response } }
35
- handle(response.body)
36
- end
37
-
38
- def verify(response)
39
- sign = response.delete(:sign)
40
- test = sign(response)
41
- logger.debug { { result: test } }
42
- sign == test[:sign]
43
- end
44
-
45
- def parse(body)
46
- parser.parse(body)
47
- end
48
-
49
- private
50
-
51
- def requires(params, keys)
52
- actual = params.keys.map(&:to_sym)
53
- return if keys == (actual & keys)
54
-
55
- raise Wechatpay::Api::Error, "missing params #{keys - actual}"
56
- end
57
-
58
- def connection
59
- Faraday.new(url: @site) do |conn|
60
- conn.request :retry
61
- conn.response :logger
62
- conn.response :raise_error
63
- conn.adapter :net_http
64
- end
65
- end
66
-
67
- def handle(body)
68
- response = parse(body)
69
- check(response)
70
- response
71
- end
72
-
73
- def parser
74
- Nori.new
75
- end
76
-
77
- def check(res)
78
- return if res['xml']['result_code'] == 'SUCCESS'
79
-
80
- handle_error(res['xml']['err_code'], res)
81
- rescue NoMethodError
82
- logger.warn { res.inspect }
83
- raise Error, 'Bad Response'
84
- end
85
-
86
- def handle_error(error_code, response)
87
- raise ERRORS[error_code] || PayError, response.inspect
88
- end
89
-
90
- def xml(hash)
91
- Gyoku.xml({ xml: hash }, key_converter: :none)
92
- end
93
-
94
- def sign(params)
95
- ordered = trim_and_sort(params)
96
- keystr = format('key=%<key>s', key: key)
97
- origin = ordered.map { |k, v| [k, v].join('=') }.push(keystr).join('&')
98
- sign = Digest::MD5.hexdigest(origin).upcase
99
- logger.debug { format('origin: %s, sign: %s', origin, sign) }
100
- params.merge(sign: sign)
101
- end
102
-
103
- def trim_and_sort(params)
104
- params.delete_if { |_k, v| v.to_s.blank? }
105
- Hash[params.sort]
106
- end
107
-
108
- def merge(params)
109
- { mch_id: @mch_id, nonce_str: nonce_str }.merge(params)
110
- end
111
-
112
- def nonce_str
113
- SecureRandom.hex
114
- end
115
- end
116
- end
117
- end
118
- end
@@ -1,26 +0,0 @@
1
- module Wechatpay
2
- module Api
3
- module V2
4
- module Pay
5
-
6
- def unifiedorder(params)
7
- requires(params, %i[body out_trade_no total_fee spbill_create_ip notify_url trade_type])
8
- post('/pay/unifiedorder', params)
9
- end
10
-
11
- def jsapi_params(params)
12
- requires(params, %i[prepayid])
13
-
14
- data = {
15
- appId: appid,
16
- package: format('prepay_id=%<prepayid>s', params),
17
- nonceStr: nonce_str,
18
- timeStamp: Time.now.to_i,
19
- signType: 'MD5'
20
- }
21
- sign(data)
22
- end
23
- end
24
- end
25
- end
26
- end