wechatpay-api 0.0.2 → 0.2.1

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.
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