jdpay 0.1.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.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "jd_pay"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/jd_pay.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jd_pay/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jdpay"
8
+ spec.version = JdPay::VERSION
9
+ spec.authors = ["Genkin He"]
10
+ spec.email = ["hemengzhi88@gmail.com"]
11
+ spec.summary = %q{An unofficial simple jdpay gem.}
12
+ spec.description = %q{An unofficial simple jdpay gem}
13
+ spec.homepage = "https://github.com/genkin-he/jd_pay"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.require_paths = ["lib"]
19
+ spec.test_files = Dir["test/**/*"]
20
+
21
+ spec.add_runtime_dependency "rest-client", '>= 2.0.0'
22
+ spec.add_runtime_dependency "activesupport", '>= 3.2'
23
+ spec.add_runtime_dependency "builder", '~> 3.2.2'
24
+
25
+ spec.add_development_dependency "bundler", '~> 1.3'
26
+ spec.add_development_dependency "rake", '~> 11.2'
27
+ spec.add_development_dependency "webmock", '~> 2.3'
28
+ spec.add_development_dependency "minitest", '~> 5'
29
+ end
data/lib/jd_pay.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "jd_pay/version"
2
+ require "jd_pay/des"
3
+ require "jd_pay/result"
4
+ require "jd_pay/service"
5
+ require "jd_pay/sign"
6
+ require "jd_pay/util"
7
+ require "jd_pay/qr_service"
8
+
9
+ module JdPay
10
+ @extra_rest_client_options = {}
11
+ @debug_mode = true
12
+
13
+ class << self
14
+ attr_accessor :mch_id, :md5_key, :des_key, :pri_key, :pub_key, :debug_mode,
15
+ :extra_rest_client_options, :qr_mch_id, :qr_des_key, :qr_pri_key
16
+ def public_key
17
+ self.pub_key ? self.pub_key : JdPay::Sign::JDPAY_RSA_PUBLIC_KEY
18
+ end
19
+
20
+ def debug_mode?
21
+ !!@debug_mode
22
+ end
23
+ end
24
+ end
data/lib/jd_pay/des.rb ADDED
@@ -0,0 +1,66 @@
1
+ module JdPay
2
+ module Des
3
+ class << self
4
+ def encrypt_3des(str, options = {})
5
+ des = OpenSSL::Cipher::Cipher.new('des-ede3')
6
+ str = format_str_data(str)
7
+ des.encrypt
8
+ des.key = decode_key(options)
9
+ des.iv = des.random_iv
10
+ str = des.update(str)
11
+ to_hex(str.bytes)
12
+ end
13
+
14
+ def decrypt_3des(encrypt_str, options = {})
15
+ encrypt_str = to_decimal(encrypt_str)
16
+ des2 = OpenSSL::Cipher::Cipher.new('des-ede3')
17
+ des2.decrypt
18
+ des2.key = decode_key(options)
19
+ des2.iv = des2.random_iv
20
+ des2.padding = 0
21
+ result = (des2.update(encrypt_str) + des2.final).bytes
22
+ result.first(valid_size(result.shift(4))).map(&:chr).join.force_encoding('utf-8').encode('utf-8')
23
+ end
24
+
25
+ private
26
+
27
+ def decode_key(options = {})
28
+ Base64.decode64(options[:des_key] || JdPay.des_key)
29
+ end
30
+
31
+ # 计算补位数据数组
32
+ def padding_array(num)
33
+ temp = (num + 4) % 8
34
+ Array.new(temp == 0 ? 0 : (8 - temp), 0x00)
35
+ end
36
+
37
+ # 有效数据长度数组
38
+ def valid_size_array(num)
39
+ size_array = [num >> 24 & 0xff, num >> 16 & 0xff, num >> 8 & 0xff, num & 0xff]
40
+ end
41
+
42
+ # 根据数组算出有效数据值
43
+ def valid_size(arr)
44
+ to_hex(arr).to_i(16)
45
+ end
46
+
47
+ # 格式化加密元数据
48
+ def format_str_data(str)
49
+ str_bytes = str.bytes
50
+ str_bytes_size = str_bytes.length
51
+ (valid_size_array(str_bytes_size) + str_bytes + padding_array(str_bytes_size)).map(&:chr).join
52
+ end
53
+
54
+ # 10进制string转16进制
55
+ def to_hex(arr)
56
+ arr.map { |num| "%02x" % num }.join
57
+ end
58
+
59
+ # 16进制string转10进制
60
+ def to_decimal(str)
61
+ # str.scan(/../).map{ |r| r.hex.chr }.join
62
+ Array(str).pack('H*')
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,17 @@
1
+ require 'rest_client'
2
+ require 'active_support/core_ext/hash/conversions'
3
+ module JdPay
4
+ module QrService
5
+ USABLE_METHODS = %i(qrcode_pay refund query revoke notify_verify)
6
+ def self.method_missing(method, *args)
7
+
8
+ super unless USABLE_METHODS.include?(method)
9
+ qr_service_default_config = {
10
+ mch_id: JdPay.qr_mch_id, des_key: JdPay.qr_des_key, pri_key: JdPay.qr_pri_key
11
+ }
12
+ args[1] = {} if args[1].nil?
13
+ args[1] = qr_service_default_config.merge(args[1])
14
+ JdPay::Service.public_send(method, *args)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ module JdPay
2
+ class Result < ::Hash
3
+ SUCCESS_FLAG = '000000'.freeze
4
+
5
+ def initialize(result, options = {})
6
+ super nil
7
+
8
+ self['jdpay'] = result['jdpay']
9
+
10
+ if result['jdpay'].class == Hash && (decrypt = self.decrypt_verify(options = {})).class == Hash
11
+ self['jdpay'] = decrypt['jdpay']
12
+ end
13
+ end
14
+
15
+ def success?
16
+ self['jdpay']['result']['code'] == SUCCESS_FLAG
17
+ end
18
+
19
+ def decrypt_verify(options = {})
20
+ if self.success?
21
+ content_hash = Hash.from_xml JdPay::Des.decrypt_3des(Base64.decode64(self['jdpay']['encrypt']), options)
22
+ JdPay::Sign.rsa_verify?(content_hash, options) ? content_hash : (raise "JdPay_verify_err:#{content_hash}")
23
+ else
24
+ raise "JdPay::Result#decrypt_verify_err:#{self}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,180 @@
1
+ require 'rest_client'
2
+ require 'active_support/core_ext/hash/conversions'
3
+
4
+ module JdPay
5
+ module Service
6
+ UNIORDER_URL = 'https://paygate.jd.com/service/uniorder'
7
+ QUERY_URL = 'https://paygate.jd.com/service/query'
8
+ REFUND_URL = 'https://paygate.jd.com/service/refund'
9
+ H5_PAY_URL = 'https://h5pay.jd.com/jdpay/saveOrder'
10
+ PC_PAY_URL = 'https://wepay.jd.com/jdpay/saveOrder'
11
+ REVOKE_URL = 'https://paygate.jd.com/service/revoke'
12
+ QRCODE_PAY_URL = 'https://paygate.jd.com/service/fkmPay'
13
+ USER_RELATION_URL = 'https://paygate.jd.com/service/getUserRelation'
14
+ CANCEL_USER_URL = 'https://paygate.jd.com/service/cancelUserRelation'
15
+
16
+ class << self
17
+ # the difference between pc and h5 is just request url
18
+ def pc_pay(params, options = {})
19
+ web_pay(params, PC_PAY_URL, options = {})
20
+ end
21
+
22
+ def h5_pay(params, options = {})
23
+ web_pay(params, H5_PAY_URL, options = {})
24
+ end
25
+
26
+ WEB_PAY_REQUIRED_FIELDS = [:tradeNum, :tradeName, :amount, :orderType, :notifyUrl, :callbackUrl, :userId]
27
+ def web_pay(params, url, options = {})
28
+ params = {
29
+ version: "V2.0",
30
+ merchant: options[:mch_id] || JdPay.mch_id,
31
+ tradeTime: Time.now.strftime("%Y%m%d%H%M%S"),
32
+ currency: "CNY"
33
+ }.merge(params)
34
+
35
+ check_required_options(params, WEB_PAY_REQUIRED_FIELDS)
36
+ sign = JdPay::Sign.rsa_encrypt(JdPay::Util.to_uri(params), options)
37
+ skip_encrypt_params = %i(version merchant)
38
+ params.each do |k, v|
39
+ params[k] = skip_encrypt_params.include?(k) ? v : JdPay::Des.encrypt_3des(v)
40
+ end
41
+ params[:sign] = sign
42
+ JdPay::Util.build_pay_form(url, params)
43
+ end
44
+
45
+ UNIORDER_REQUIRED_FIELDS = [:tradeNum, :tradeName, :amount, :orderType, :notifyUrl, :userId]
46
+ def uniorder(params, options = {})
47
+ params = {
48
+ version: "V2.0",
49
+ merchant: options[:mch_id] || JdPay.mch_id,
50
+ tradeTime: Time.now.strftime("%Y%m%d%H%M%S"),
51
+ currency: "CNY"
52
+ }.merge(params)
53
+
54
+ check_required_options(params, UNIORDER_REQUIRED_FIELDS)
55
+ params[:sign] = JdPay::Sign.rsa_encrypt(JdPay::Util.to_xml(params), options)
56
+
57
+ JdPay::Result.new(Hash.from_xml(invoke_remote(UNIORDER_URL, make_payload(params), options)), options)
58
+ end
59
+
60
+ QRCODE_REQUIRED_FIELDS = [:tradeNum, :tradeName, :amount, :device, :token]
61
+ def qrcode_pay(params, options = {})
62
+ params = {
63
+ version: "V2.0",
64
+ merchant: options[:mch_id] || JdPay.mch_id,
65
+ tradeTime: Time.now.strftime("%Y%m%d%H%M%S"),
66
+ currency: "CNY"
67
+ }.merge(params)
68
+
69
+ check_required_options(params, QRCODE_REQUIRED_FIELDS)
70
+ params[:sign] = JdPay::Sign.rsa_encrypt(JdPay::Util.to_xml(params), options)
71
+
72
+ JdPay::Result.new(Hash.from_xml(invoke_remote(QRCODE_PAY_URL, make_payload(params), options)), options)
73
+ end
74
+
75
+ USER_RELATION_REQUIRED_FIELDS = [:useId]
76
+ def user_relation(params, options = {})
77
+ params = {
78
+ version: "V2.0",
79
+ merchant: options[:mch_id] || JdPay.mch_id,
80
+ }.merge(params)
81
+
82
+ check_required_options(params, USER_RELATION_REQUIRED_FIELDS)
83
+ params[:sign] = JdPay::Sign.rsa_encrypt(JdPay::Util.to_xml(params), options)
84
+
85
+ JdPay::Result.new(Hash.from_xml(invoke_remote(USER_RELATION_URL, make_payload(params), options)), options)
86
+ end
87
+
88
+ def cancel_user(params, options = {})
89
+ params = {
90
+ version: "V2.0",
91
+ merchant: options[:mch_id] || JdPay.mch_id,
92
+ }.merge(params)
93
+
94
+ check_required_options(params, USER_RELATION_REQUIRED_FIELDS)
95
+ params[:sign] = JdPay::Sign.rsa_encrypt(JdPay::Util.to_xml(params), options)
96
+
97
+ JdPay::Result.new(Hash.from_xml(invoke_remote(CANCEL_USER_URL, make_payload(params), options)), options)
98
+ end
99
+
100
+ REFUND_REQUIRED_FIELDS = [:tradeNum, :oTradeNum, :amount, :notifyUrl]
101
+ def refund(params, options = {})
102
+ params = {
103
+ version: "V2.0",
104
+ merchant: options[:mch_id] || JdPay.mch_id,
105
+ tradeTime: Time.now.strftime("%Y%m%d%H%M%S"),
106
+ currency: "CNY"
107
+ }.merge(params)
108
+
109
+ check_required_options(params, REFUND_REQUIRED_FIELDS)
110
+ params[:sign] = JdPay::Sign.rsa_encrypt(JdPay::Util.to_xml(params), options)
111
+
112
+ JdPay::Result.new(Hash.from_xml(invoke_remote(REFUND_URL, make_payload(params), options)), options)
113
+ end
114
+
115
+ QUERY_REQUIRED_FIELDS = [:tradeNum, :tradeType]
116
+ def query(params, options = {})
117
+ params = {
118
+ version: "V2.0",
119
+ merchant: options[:mch_id] || JdPay.mch_id,
120
+ tradeType: '0'
121
+ }.merge(params)
122
+
123
+ check_required_options(params, QUERY_REQUIRED_FIELDS)
124
+ params[:sign] = JdPay::Sign.rsa_encrypt(JdPay::Util.to_xml(params), options)
125
+
126
+ JdPay::Result.new(Hash.from_xml(invoke_remote(QUERY_URL, make_payload(params), options)), options)
127
+ end
128
+
129
+ REVOKE_REQUIRED_FIELDS = [:tradeNum, :oTradeNum, :amount]
130
+ def revoke(params, options = {})
131
+ params = {
132
+ version: "V2.0",
133
+ merchant: options[:mch_id] || JdPay.mch_id,
134
+ tradeTime: Time.now.strftime("%Y%m%d%H%M%S"),
135
+ currency: "CNY"
136
+ }.merge(params)
137
+
138
+ check_required_options(params, REVOKE_REQUIRED_FIELDS)
139
+ params[:sign] = JdPay::Sign.rsa_encrypt(JdPay::Util.to_xml(params), options)
140
+
141
+ JdPay::Result.new(Hash.from_xml(invoke_remote(REVOKE_URL, make_payload(params), options)), options)
142
+ end
143
+
144
+ def notify_verify(xml_str, options = {})
145
+ JdPay::Result.new(Hash.from_xml(xml_str), options)
146
+ end
147
+
148
+ private
149
+
150
+ def check_required_options(options, names)
151
+ return unless JdPay.debug_mode?
152
+ names.each do |name|
153
+ warn("JdPay Warn: missing required option: #{name}") unless options.has_key?(name)
154
+ end
155
+ end
156
+
157
+ def make_payload(params, options = {})
158
+ request_hash = {
159
+ "version" => "V2.0",
160
+ "merchant" => options[:mch_id] || JdPay.mch_id,
161
+ "encrypt" => Base64.strict_encode64(JdPay::Des.encrypt_3des JdPay::Util.to_xml(params))
162
+ }
163
+ JdPay::Util.to_xml(request_hash)
164
+ end
165
+
166
+ def invoke_remote(url, payload, options = {})
167
+ options = JdPay.extra_rest_client_options.merge(options)
168
+
169
+ RestClient::Request.execute(
170
+ {
171
+ method: :post,
172
+ url: url,
173
+ payload: payload,
174
+ headers: { content_type: 'application/xml' }
175
+ }.merge(options)
176
+ )
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,47 @@
1
+ require 'digest/md5'
2
+ require "base64"
3
+
4
+ module JdPay
5
+ module Sign
6
+
7
+ JDPAY_RSA_PUBLIC_KEY = <<EOF
8
+ -----BEGIN PUBLIC KEY-----
9
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCKE5N2xm3NIrXON8Zj19GNtLZ8
10
+ xwEQ6uDIyrS3S03UhgBJMkGl4msfq4Xuxv6XUAN7oU1XhV3/xtabr9rXto4Ke3d6
11
+ WwNbxwXnK5LSgsQc1BhT5NcXHXpGBdt7P8NMez5qGieOKqHGvT0qvjyYnYA29a8Z
12
+ 4wzNR7vAVHp36uD5RwIDAQAB
13
+ -----END PUBLIC KEY-----
14
+ EOF
15
+
16
+ class << self
17
+ # params:
18
+ # :orderId
19
+ def md5_sign(order_id, options = {})
20
+ Digest::MD5.hexdigest(
21
+ "merchant=#{options[:mch_id] || JdPay.mch_id}" +
22
+ "&orderId=#{order_id}&key=#{options[:md5_key] || JdPay.md5_key}"
23
+ )
24
+ end
25
+
26
+ def rsa_encrypt(str, options = {})
27
+ private_key = OpenSSL::PKey::RSA.new(options[:pri_key] || JdPay.pri_key)
28
+ Base64.strict_encode64(private_key.private_encrypt Digest::SHA256.hexdigest(str))
29
+ end
30
+
31
+ def rsa_decrypt(sign_str, options = {})
32
+ public_key = OpenSSL::PKey::RSA.new(options[:pub_key] || JdPay.public_key)
33
+ public_key.public_decrypt(Base64.decode64(sign_str))
34
+ end
35
+
36
+ def rsa_verify?(params, options = {})
37
+ params = params['jdpay'].dup
38
+ sign_str = params.delete('sign')
39
+ xml_without_sign = JdPay::Util.to_xml(params, root: 'jdpay')
40
+ ori_datas = [xml_without_sign, xml_without_sign.gsub("?>", " ?>")].map do |xml|
41
+ Digest::SHA256.hexdigest(xml)
42
+ end
43
+ ori_datas.include? rsa_decrypt(sign_str, options)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ module JdPay
2
+ module Util
3
+ class << self
4
+ def to_xml(params, options = {})
5
+ options[:root] = options[:root] || 'jdpay'
6
+ denilize(params).to_xml(options).gsub(/>[[:space:]]+/, ">")
7
+ end
8
+
9
+ def to_uri(params)
10
+ params.sort.map do |k, v|
11
+ "#{k}=#{v}"
12
+ end.compact.join('&')
13
+ end
14
+
15
+ def denilize(h)
16
+ h.each_with_object({}) { | (k, v), g |
17
+ g[k] = (Hash === v) ? denilize(v) : v ? v : '' }
18
+ end
19
+
20
+ def build_pay_form(url, form_attributes)
21
+ inputs = ''
22
+ "<html>
23
+ <body onload=document.getElementById('payForm').submit(); style='display: none;'>
24
+ <form action=#{url} method='post' id='payForm'>
25
+ #{form_attributes.each do |k, v|
26
+ inputs << "<input type='text' name=#{k} value=#{v}>"
27
+ end and inputs}
28
+ </form>
29
+ </body>
30
+ </html>".gsub(/>[[:space:]]+/, ">")
31
+ end
32
+ end
33
+ end
34
+ end