xi_wechat_corp 1.0.0
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 +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +86 -0
- data/Rakefile +9 -0
- data/lib/xi_wechat_corp/aes_crypt.rb +77 -0
- data/lib/xi_wechat_corp/api/access_token.rb +42 -0
- data/lib/xi_wechat_corp/api/connection.rb +99 -0
- data/lib/xi_wechat_corp/api.rb +2 -0
- data/lib/xi_wechat_corp/callback/config.rb +45 -0
- data/lib/xi_wechat_corp/callback/rack.rb +63 -0
- data/lib/xi_wechat_corp/callback/request.rb +82 -0
- data/lib/xi_wechat_corp/callback/response.rb +31 -0
- data/lib/xi_wechat_corp/callback.rb +7 -0
- data/lib/xi_wechat_corp/error.rb +13 -0
- data/lib/xi_wechat_corp/pkcs7_encoder.rb +28 -0
- data/lib/xi_wechat_corp/sha1_signer.rb +14 -0
- data/lib/xi_wechat_corp/version.rb +3 -0
- data/lib/xi_wechat_corp.rb +12 -0
- data/test/test_helper.rb +12 -0
- data/test/xi_wechat_corp/aes_crypt_test.rb +24 -0
- data/test/xi_wechat_corp/api/access_token_test.rb +22 -0
- data/test/xi_wechat_corp/api/connection_test.rb +36 -0
- data/test/xi_wechat_corp/callback/rack_test.rb +73 -0
- data/test/xi_wechat_corp/pkcs7_encoder_test.rb +29 -0
- data/test/xi_wechat_corp/sha1_signer_test.rb +19 -0
- data/xi_wechat_corp.gemspec +31 -0
- metadata +218 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a668aecce2c27bf2ecf30019fe33a0a7e03778a9
|
4
|
+
data.tar.gz: 99ea5c52fe837b0e3dc317de78325a6e01566ba6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3257dd7d6467d516ba196d8be07f635bf826d6dacfd6d77c17ad21c038b67fdc12c9804ac4d53cd9323f23eb57108d0406788151ea23cc0cd815d43c7f46d059
|
7
|
+
data.tar.gz: 4496688d3f9aa96234d5f538e1d6b69013e44995a924bb8eb1d037109b77292900822f9d04b5da4ec885376d8a27dc92c73a1ce540549c1f9de5a27153c32f64
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Ian Yang
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# XiWechatCorp
|
2
|
+
|
3
|
+
Toolkits for Tencent Wechat Corp development
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'xi_wechat_corp'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install xi_wechat_corp
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### Rack Middleware
|
22
|
+
|
23
|
+
use XiWechatCorp::Callback::Rack do |request|
|
24
|
+
corp_id 'wx1234'
|
25
|
+
if request.path_info == '/wechat'
|
26
|
+
token 'secret'
|
27
|
+
aes_key 'secret'
|
28
|
+
elsif request.path_info == '/another'
|
29
|
+
token 'secret'
|
30
|
+
aes_key 'secret'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
The middleware is enabled for requests when `corp_id`, `token` and `aes_key`
|
35
|
+
are all configured.
|
36
|
+
|
37
|
+
The `GET` request is handled by the middleware directly. Just implement the
|
38
|
+
logic for `POST` requests. All post params can be get through rack env
|
39
|
+
`xi_wechat_corp.params`, including decrypted params. The handler just return
|
40
|
+
the plain XML, The middleware will encrypt response and sign it.
|
41
|
+
|
42
|
+
### Handle Callback Manually
|
43
|
+
|
44
|
+
Verify and decrypt Request
|
45
|
+
|
46
|
+
cryptor = XiWechatCorp::AesCrypt.new(aes_key, corp_id)
|
47
|
+
signer = XiWechatCorp::SHA1Signer.new(token)
|
48
|
+
request = XiWechatCorp::Callback::Request.new(cryptor, signer, query_params, xml_body)
|
49
|
+
request.verify!
|
50
|
+
|
51
|
+
# Get decrypted echostr for GET
|
52
|
+
request.challenge
|
53
|
+
|
54
|
+
# Decrypt Encrypt field for POST
|
55
|
+
request.decrypt
|
56
|
+
|
57
|
+
# Pack response
|
58
|
+
response = request.build_response
|
59
|
+
response.assign(xml_response)
|
60
|
+
response.to_s
|
61
|
+
|
62
|
+
### API
|
63
|
+
|
64
|
+
conn = XiWechatCorp::Client::Connection.new
|
65
|
+
conn.get_access_token(corp_id, secret)
|
66
|
+
conn.post(
|
67
|
+
'message/send',
|
68
|
+
touser: 'UserID1|UserID2|UserID3',
|
69
|
+
toparty: ' PartyID1 | PartyID2 ',
|
70
|
+
totag: ' TagID1 | TagID2 ',
|
71
|
+
touser: 'user1|user2',
|
72
|
+
msgtype: 'text',
|
73
|
+
agentid: '1',
|
74
|
+
text: {
|
75
|
+
content: 'Holiday Request for Pony'
|
76
|
+
},
|
77
|
+
safe: 0
|
78
|
+
)
|
79
|
+
|
80
|
+
## Contributing
|
81
|
+
|
82
|
+
1. Fork it ( https://github.com/3pjgames/xi_wechat_corp/fork )
|
83
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
84
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
85
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
86
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'base64'
|
3
|
+
require_relative 'error'
|
4
|
+
require_relative 'pkcs7_encoder'
|
5
|
+
|
6
|
+
module XiWechatCorp
|
7
|
+
class AesCrypt
|
8
|
+
PKCS7 = PKCS7Encoder.new
|
9
|
+
|
10
|
+
def initialize(aes_key, corp_id)
|
11
|
+
@aes_key = decode64(aes_key + '=')
|
12
|
+
raise ArgumentError if @aes_key.size != 32
|
13
|
+
@corp_id = corp_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def encrypt(text)
|
17
|
+
encode64 cipher_encrypt pad pack text
|
18
|
+
end
|
19
|
+
|
20
|
+
def decrypt(text)
|
21
|
+
unpack unpad cipher_decrypt decode64 text
|
22
|
+
end
|
23
|
+
|
24
|
+
def pack(text)
|
25
|
+
[
|
26
|
+
SecureRandom.random_bytes(16),
|
27
|
+
[text.bytesize].pack('N'),
|
28
|
+
text,
|
29
|
+
@corp_id
|
30
|
+
].join('')
|
31
|
+
end
|
32
|
+
|
33
|
+
def unpack(text)
|
34
|
+
size, corp_id = text.unpack('@16Na*')
|
35
|
+
output = corp_id.slice!(0, size)
|
36
|
+
|
37
|
+
raise InvalidCorpIDError.new(corp_id) if corp_id != @corp_id
|
38
|
+
output
|
39
|
+
end
|
40
|
+
|
41
|
+
def pad(text)
|
42
|
+
PKCS7.encode(text)
|
43
|
+
end
|
44
|
+
|
45
|
+
def unpad(text)
|
46
|
+
PKCS7.decode(text)
|
47
|
+
end
|
48
|
+
|
49
|
+
def encode64(text)
|
50
|
+
Base64.strict_encode64(text)
|
51
|
+
end
|
52
|
+
|
53
|
+
def decode64(text)
|
54
|
+
Base64.decode64(text)
|
55
|
+
end
|
56
|
+
|
57
|
+
def cipher_encrypt(text)
|
58
|
+
cipher(:encrypt, text)
|
59
|
+
end
|
60
|
+
|
61
|
+
def cipher_decrypt(text)
|
62
|
+
cipher(:decrypt, text)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def cipher(action, text)
|
68
|
+
cipher = OpenSSL::Cipher::AES.new(256, :CBC)
|
69
|
+
cipher.send action
|
70
|
+
cipher.padding = 0
|
71
|
+
cipher.key = @aes_key
|
72
|
+
cipher.iv = @aes_key[0...16]
|
73
|
+
cipher.update(text) + cipher.final
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module XiWechatCorp
|
2
|
+
module API
|
3
|
+
class AccessToken
|
4
|
+
# Leave 5 minutes buffer
|
5
|
+
EXPIRE_SECONDS = 7200 - 300
|
6
|
+
|
7
|
+
attr_reader :token
|
8
|
+
attr_reader :expired_at
|
9
|
+
|
10
|
+
def initialize(token = nil, expired_at = nil)
|
11
|
+
assign(token, expired_at)
|
12
|
+
end
|
13
|
+
|
14
|
+
def assign(token, expired_at = nil)
|
15
|
+
@token = token.to_s
|
16
|
+
if !@token.empty?
|
17
|
+
@expired_at = expired_at || (token.respond_to?(:expired_at) ? token.expired_at : Time.now.to_i + EXPIRE_SECONDS)
|
18
|
+
else
|
19
|
+
@expired_at = 0
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method :refresh, :assign
|
24
|
+
|
25
|
+
def present?
|
26
|
+
@token.size > 0
|
27
|
+
end
|
28
|
+
|
29
|
+
def expired?
|
30
|
+
@token.empty? || @expired_at < Time.now.to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
def valid?
|
34
|
+
!expired?
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
@token
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'faraday_middleware'
|
3
|
+
require_relative 'access_token'
|
4
|
+
|
5
|
+
module XiWechatCorp
|
6
|
+
module API
|
7
|
+
Error = Faraday::Error
|
8
|
+
class ClientError < Faraday::ClientError
|
9
|
+
def initialize(response)
|
10
|
+
@response = response
|
11
|
+
message = response ? response.values_at('errcode', 'errmsg').compact.join(' ') : nil
|
12
|
+
super(message)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class RaiseClientError < Faraday::Response::Middleware
|
17
|
+
def parse(body)
|
18
|
+
raise ClientError.new(body) if body.nil? || body.key?('errcode')
|
19
|
+
body
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Connection < Faraday::Connection
|
24
|
+
ENDPOINT = 'https://qyapi.weixin.qq.com/cgi-bin'
|
25
|
+
|
26
|
+
BUILDER = Faraday::RackBuilder.new do |builder|
|
27
|
+
builder.request :json
|
28
|
+
builder.use RaiseClientError
|
29
|
+
builder.response :json
|
30
|
+
builder.response :raise_error
|
31
|
+
builder.adapter Faraday.default_adapter
|
32
|
+
end
|
33
|
+
|
34
|
+
ACCESS_TOKEN_MISSING = [41001, 'Access token is missing']
|
35
|
+
ACCESS_TOKEN_EXPIRED = [40014, 'Access token is expired']
|
36
|
+
|
37
|
+
# Accepts all options supported by Faraday::Connection, as well as:
|
38
|
+
#
|
39
|
+
# - access_token [AccessToken|String] already acquired access token
|
40
|
+
# - corp_id
|
41
|
+
# - secret
|
42
|
+
#
|
43
|
+
# When both corp_id and secret are specified, access token is fetched and
|
44
|
+
# refreshed automatically.
|
45
|
+
def initialize(options = {}, &block)
|
46
|
+
token = options.delete(:access_token)
|
47
|
+
token = options[:params][:access_token] if token.nil? && options.key?(:params)
|
48
|
+
@access_token = AccessToken.new(options.delete(:access_token))
|
49
|
+
|
50
|
+
@corp_id = options.delete(:corp_id)
|
51
|
+
@secret = options.delete(:secret)
|
52
|
+
|
53
|
+
super(ENDPOINT, { builder: BUILDER }.merge(options), &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
extend Forwardable
|
57
|
+
def_delegator :@access_token, :assign, :access_token=
|
58
|
+
|
59
|
+
def get_access_token(corp_id = nil, secret = nil)
|
60
|
+
@corp_id = corp_id unless corp_id.nil?
|
61
|
+
@secret = secret unless secret.nil?
|
62
|
+
resp = get('gettoken', corpid: @corp_id, corpsecret: @secret)
|
63
|
+
@access_token.assign(resp.body['access_token'])
|
64
|
+
@access_token
|
65
|
+
end
|
66
|
+
|
67
|
+
alias_method :auth, :get_access_token
|
68
|
+
|
69
|
+
def run_request(method, url, body, headers, &block)
|
70
|
+
set_access_token_in_params unless url == 'gettoken'
|
71
|
+
super(method, url, body, headers, &block)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def can_get_access_token?
|
76
|
+
!@corp_id.nil? && !@secret.nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
def error!(code, msg)
|
80
|
+
raise ClientError.new('errcode' => code, 'errmsg' => msg)
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_access_token_in_params
|
84
|
+
if @access_token.expired?
|
85
|
+
if can_get_access_token?
|
86
|
+
get_access_token
|
87
|
+
elsif @access_token.present?
|
88
|
+
error!(*ACCESS_TOKEN_EXPIRED)
|
89
|
+
else
|
90
|
+
error!(*ACCESS_TOKEN_MISSING)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
params['access_token'] = @access_token
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
require 'xi_wechat_corp/sha1_signer'
|
3
|
+
require 'xi_wechat_corp/aes_crypt'
|
4
|
+
require_relative 'request'
|
5
|
+
|
6
|
+
module XiWechatCorp
|
7
|
+
module Callback
|
8
|
+
class Config
|
9
|
+
include Rack::Utils
|
10
|
+
|
11
|
+
class Credentials
|
12
|
+
def corp_id(v = nil)
|
13
|
+
v.nil? ? @corp_id : (@corp_id = v)
|
14
|
+
end
|
15
|
+
def token(v = nil)
|
16
|
+
v.nil? ? @token : (@token = v)
|
17
|
+
end
|
18
|
+
def aes_key(v = nil)
|
19
|
+
v.nil? ? @aes_key : (@aes_key = v)
|
20
|
+
end
|
21
|
+
|
22
|
+
def configured?
|
23
|
+
@corp_id && @token && @aes_key
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def configure(&block)
|
28
|
+
@block = block
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_request(rack_request)
|
33
|
+
return if @block.nil?
|
34
|
+
credentials = Credentials.new
|
35
|
+
credentials.instance_exec(rack_request, &@block)
|
36
|
+
if credentials.configured?
|
37
|
+
cryptor = AesCrypt.new(credentials.aes_key, credentials.corp_id)
|
38
|
+
signer = SHA1Signer.new(credentials.token)
|
39
|
+
query_params = parse_query(rack_request.query_string)
|
40
|
+
Request.new(cryptor, signer, query_params, rack_request.body)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
require 'xi_wechat_corp'
|
3
|
+
require 'xi_wechat_corp/error'
|
4
|
+
require_relative 'request'
|
5
|
+
require_relative 'response'
|
6
|
+
require_relative 'config'
|
7
|
+
|
8
|
+
module XiWechatCorp
|
9
|
+
module Callback
|
10
|
+
class Rack
|
11
|
+
include ::Rack::Utils
|
12
|
+
CONTENT_TYPE = 'text/html; charset=utf-8'
|
13
|
+
|
14
|
+
def initialize(app, &block)
|
15
|
+
@app = app
|
16
|
+
@config = Config.new.configure(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
rack_request = ::Rack::Request.new(env)
|
21
|
+
if (rack_request.get? || rack_request.post?) &&
|
22
|
+
(request = @config.build_request(rack_request))
|
23
|
+
request.verify!
|
24
|
+
if rack_request.get?
|
25
|
+
::Rack::Response.new([request.challenge]).finish
|
26
|
+
else
|
27
|
+
request.decrypt
|
28
|
+
env['xi_wechat_corp.params'] = request.params
|
29
|
+
status, headers, body = @app.call(env)
|
30
|
+
rack_response = ::Rack::Response.new([], status, headers)
|
31
|
+
if rack_response.successful?
|
32
|
+
response = request.build_response(read_body(body))
|
33
|
+
xml = response.to_s
|
34
|
+
if !xml.empty?
|
35
|
+
rack_response['Content-Type'] = CONTENT_TYPE
|
36
|
+
rack_response.write xml
|
37
|
+
end
|
38
|
+
rack_response.finish
|
39
|
+
else
|
40
|
+
[status, headers, body]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
else
|
44
|
+
@app.call(env)
|
45
|
+
end
|
46
|
+
rescue XiWechatCorp::Error => e
|
47
|
+
if XiWechatCorp.logger
|
48
|
+
XiWechatCorp.logger.error(e.message)
|
49
|
+
end
|
50
|
+
[403, {}, []]
|
51
|
+
end
|
52
|
+
|
53
|
+
def read_body(body)
|
54
|
+
ret = ''
|
55
|
+
body.each do |str|
|
56
|
+
ret << str
|
57
|
+
end
|
58
|
+
body.close if body.respond_to?(:close)
|
59
|
+
ret
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'multi_xml'
|
2
|
+
require_relative 'response'
|
3
|
+
|
4
|
+
module XiWechatCorp
|
5
|
+
module Callback
|
6
|
+
class Request
|
7
|
+
SIGN_PARAMS = %w(timestamp nonce echostr Encrypt)
|
8
|
+
|
9
|
+
attr_reader :params
|
10
|
+
|
11
|
+
def initialize(cryptor, signer, query_params = {}, xml = nil)
|
12
|
+
@cryptor = cryptor
|
13
|
+
@signer = signer
|
14
|
+
assign(query_params, xml)
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_response(xml = nil)
|
18
|
+
Response.new(@cryptor, @signer, xml)
|
19
|
+
end
|
20
|
+
|
21
|
+
def assign(query_params, xml = nil)
|
22
|
+
body_params = xml ? parse_xml(xml) : {}
|
23
|
+
@params = body_params.merge(query_params)
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](key)
|
27
|
+
@params[key.to_s]
|
28
|
+
end
|
29
|
+
|
30
|
+
def key?(key)
|
31
|
+
@params.key?(key.to_s)
|
32
|
+
end
|
33
|
+
|
34
|
+
def verify
|
35
|
+
signature = @params['msg_signature']
|
36
|
+
signature && signature == @signer.sign(*@params.values_at(*SIGN_PARAMS))
|
37
|
+
end
|
38
|
+
|
39
|
+
def verify!
|
40
|
+
raise SignatureVerificationError unless verify
|
41
|
+
end
|
42
|
+
|
43
|
+
def challenge?
|
44
|
+
key?('echostr')
|
45
|
+
end
|
46
|
+
|
47
|
+
def challenge
|
48
|
+
@cryptor.decrypt(@params['echostr'])
|
49
|
+
end
|
50
|
+
|
51
|
+
def decryptable?
|
52
|
+
key?('Encrypt')
|
53
|
+
end
|
54
|
+
|
55
|
+
def decrypt
|
56
|
+
if decryptable?
|
57
|
+
@params.merge!(parse_xml(@cryptor.decrypt(@params['Encrypt'])))
|
58
|
+
end
|
59
|
+
@params
|
60
|
+
end
|
61
|
+
|
62
|
+
def corp_id
|
63
|
+
@params['ToUserName']
|
64
|
+
end
|
65
|
+
|
66
|
+
def agent_id
|
67
|
+
@params['AgentID']
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def parse_xml(xml)
|
73
|
+
node = MultiXml.parse(xml)
|
74
|
+
while node.key?('xml')
|
75
|
+
node = node['xml']
|
76
|
+
end
|
77
|
+
node
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module XiWechatCorp
|
4
|
+
module Callback
|
5
|
+
class Response
|
6
|
+
TEMPLATE = '<xml><Encrypt>%s</Encrypt><MsgSignature>%s</MsgSignature><TimeStamp>%d</TimeStamp><Nonce>%d</Nonce></xml>'
|
7
|
+
NONCE_UPPER = 1 << 32
|
8
|
+
|
9
|
+
def initialize(cryptor, signer, xml = nil)
|
10
|
+
@cryptor = cryptor
|
11
|
+
@signer = signer
|
12
|
+
assign(xml)
|
13
|
+
end
|
14
|
+
|
15
|
+
def assign(xml)
|
16
|
+
@encrypt = xml ? @cryptor.encrypt(xml) : nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
return '' if @encrypt.nil?
|
21
|
+
timestamp = Time.now.to_i
|
22
|
+
nonce = SecureRandom.random_number(NONCE_UPPER)
|
23
|
+
signature = @signer.sign(@encrypt, timestamp, nonce)
|
24
|
+
sprintf(TEMPLATE, @encrypt, signature, timestamp, nonce)
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_method :to_xml, :to_s
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module XiWechatCorp
|
2
|
+
class Error < StandardError; end
|
3
|
+
class DecodeError < Error; end
|
4
|
+
class PKCS7DecodeError < DecodeError; end
|
5
|
+
class InvalidCorpIDError < Error
|
6
|
+
attr_reader :corpid
|
7
|
+
def initialize(corpid)
|
8
|
+
@corpid = corpid
|
9
|
+
super "Invalid corpid: #{@corpid}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
class SignatureVerificationError < Error; end
|
13
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'error'
|
2
|
+
|
3
|
+
module XiWechatCorp
|
4
|
+
# This file is modified from offical python example
|
5
|
+
#
|
6
|
+
# http://qydev.weixin.qq.com/python.zip
|
7
|
+
class PKCS7Encoder
|
8
|
+
BLOCK_SIZE = 32
|
9
|
+
|
10
|
+
# Pad the text so the length is divisable by BLOCK_SIZE. Save the padding length as padding chracter itself.
|
11
|
+
def encode(text)
|
12
|
+
return 32.chr * BLOCK_SIZE if text.nil? || text.empty?
|
13
|
+
|
14
|
+
text_length = text.bytesize
|
15
|
+
amount_to_pad = BLOCK_SIZE - (text_length % BLOCK_SIZE)
|
16
|
+
pad = amount_to_pad.chr
|
17
|
+
text + pad * amount_to_pad
|
18
|
+
end
|
19
|
+
|
20
|
+
def decode(decrypted)
|
21
|
+
return decrypted if decrypted.nil? || decrypted.empty?
|
22
|
+
|
23
|
+
pad = decrypted[-1].ord
|
24
|
+
raise PKCS7DecodeError if pad < 1 or pad > BLOCK_SIZE
|
25
|
+
decrypted.byteslice(0...-pad)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module XiWechatCorp
|
2
|
+
# Sign and verify the signature
|
3
|
+
class SHA1Signer
|
4
|
+
def initialize(token)
|
5
|
+
@token = token
|
6
|
+
end
|
7
|
+
|
8
|
+
def sign(*args)
|
9
|
+
sha1 = Digest::SHA1.new
|
10
|
+
[@token, *(args.compact)].map(&:to_s).sort.each {|s| sha1 << s }
|
11
|
+
sha1.hexdigest
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "xi_wechat_corp/version"
|
2
|
+
|
3
|
+
module XiWechatCorp
|
4
|
+
autoload :API, 'xi_wechat_corp/api'
|
5
|
+
autoload :Callback, 'xi_wechat_corp/callback'
|
6
|
+
autoload :SHA1Signer, 'xi_wechat_corp/sha1_signer'
|
7
|
+
autoload :AesCrypt, 'xi_wechat_corp/aes_crypt'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :logger
|
11
|
+
end
|
12
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'xi_wechat_corp'
|
2
|
+
require 'minitest'
|
3
|
+
require 'minitest/unit'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'minitest/pride'
|
6
|
+
require 'webmock/minitest'
|
7
|
+
|
8
|
+
class << Minitest::Test
|
9
|
+
def test(name, &block)
|
10
|
+
define_method("test_#{name.gsub(/[^_a-zA-Z0-9]/, '_')}", &block)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'xi_wechat_corp/aes_crypt'
|
3
|
+
|
4
|
+
module XiWechatCorp
|
5
|
+
class AesCryptTest < Minitest::Test
|
6
|
+
TOKEN = "QDG6eK"
|
7
|
+
AES_KEY = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C"
|
8
|
+
CORP_ID = "wx5823bf96d3bd56c7"
|
9
|
+
ENCRYPTED = 'RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q=='
|
10
|
+
EXPECT = "<xml><ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName>\n<FromUserName><![CDATA[mycreate]]></FromUserName>\n<CreateTime>1409659813</CreateTime>\n<MsgType><![CDATA[text]]></MsgType>\n<Content><![CDATA[hello]]></Content>\n<MsgId>4561255354251345929</MsgId>\n<AgentID>218</AgentID>\n</xml>"
|
11
|
+
|
12
|
+
def test_decrypt_sample_request
|
13
|
+
crypt = AesCrypt.new(AES_KEY, CORP_ID)
|
14
|
+
text = crypt.decrypt(ENCRYPTED)
|
15
|
+
assert_equal EXPECT, text
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_encrypt_and_decrypt
|
19
|
+
crypt = AesCrypt.new(AES_KEY, CORP_ID)
|
20
|
+
text = crypt.decrypt(crypt.encrypt(EXPECT))
|
21
|
+
assert_equal EXPECT, text
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'xi_wechat_corp/api/access_token'
|
3
|
+
|
4
|
+
module XiWechatCorp
|
5
|
+
module API
|
6
|
+
class AccessTokenTest < Minitest::Test
|
7
|
+
def test_assign_access_token_to_another
|
8
|
+
@src = AccessToken.new('test', 123)
|
9
|
+
@dest = AccessToken.new
|
10
|
+
@dest.assign @src
|
11
|
+
assert_equal 'test', @dest.token
|
12
|
+
assert_equal 123, @dest.expired_at
|
13
|
+
end
|
14
|
+
def test_assign_nil
|
15
|
+
@token = AccessToken.new('test', 123)
|
16
|
+
@token.assign nil
|
17
|
+
assert_equal '', @token.token
|
18
|
+
assert_equal 0, @token.expired_at
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'xi_wechat_corp/api/access_token'
|
3
|
+
|
4
|
+
module XiWechatCorp
|
5
|
+
module API
|
6
|
+
class ConnectionTest < Minitest::Test
|
7
|
+
def setup
|
8
|
+
stub_request(:any, %r{https://qyapi.weixin.qq.com/cgi-bin/.*}).to_return(body: '{"access_token":"at"}')
|
9
|
+
end
|
10
|
+
|
11
|
+
def conn
|
12
|
+
@conn ||= Connection.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_that_access_token_is_set
|
16
|
+
conn.access_token = '123'
|
17
|
+
conn.get 'test'
|
18
|
+
assert_requested :get, 'https://qyapi.weixin.qq.com/cgi-bin/test',
|
19
|
+
query: { 'access_token' => '123' }
|
20
|
+
end
|
21
|
+
|
22
|
+
test 'raise when access token is not available' do
|
23
|
+
assert_raises ClientError do
|
24
|
+
conn.get 'test'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
test 'auto get access token' do
|
29
|
+
conn = Connection.new corp_id: 'corptest', secret: 'secret'
|
30
|
+
conn.get 'test'
|
31
|
+
assert_requested :get, 'https://qyapi.weixin.qq.com/cgi-bin/test',
|
32
|
+
query: { 'access_token' => 'at' }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'xi_wechat_corp/callback/rack'
|
3
|
+
require 'rack'
|
4
|
+
require 'rack/test'
|
5
|
+
require 'multi_xml'
|
6
|
+
|
7
|
+
module XiWechatCorp
|
8
|
+
module Callback
|
9
|
+
class RackTest < Minitest::Test
|
10
|
+
include ::Rack::Test::Methods
|
11
|
+
class App
|
12
|
+
def call(env)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
RESPONSE_XML = '<xml><test>1</test></xml>'
|
17
|
+
CORP_ID = 'corp_id'
|
18
|
+
AES_KEY = Base64.strict_encode64('a' * 32).chop
|
19
|
+
TOKEN = 'token'
|
20
|
+
|
21
|
+
def app
|
22
|
+
::Rack::Builder.app do
|
23
|
+
use Rack do |req|
|
24
|
+
if req.path_info == '/wechat'
|
25
|
+
corp_id CORP_ID
|
26
|
+
aes_key AES_KEY
|
27
|
+
token TOKEN
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
run lambda { |env|
|
32
|
+
response = if env['xi_wechat_corp.params']
|
33
|
+
env['xi_wechat_corp.params']['response']
|
34
|
+
else
|
35
|
+
RESPONSE_XML
|
36
|
+
end
|
37
|
+
[200, {'Content-Type' => 'text/xml'}, [response]]
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def cryptor
|
43
|
+
@cryptor ||= AesCrypt.new(AES_KEY, CORP_ID)
|
44
|
+
end
|
45
|
+
|
46
|
+
def signer
|
47
|
+
@signer ||= SHA1Signer.new(TOKEN)
|
48
|
+
end
|
49
|
+
|
50
|
+
test 'GET not configured path' do
|
51
|
+
get '/test'
|
52
|
+
assert_equal RESPONSE_XML, last_response.body
|
53
|
+
end
|
54
|
+
|
55
|
+
test 'GET configured path' do
|
56
|
+
echostr = cryptor.encrypt('echostr')
|
57
|
+
signature = signer.sign(1, 2, echostr)
|
58
|
+
get '/wechat', timestamp: 1, nonce: 2, msg_signature: signature, echostr: echostr
|
59
|
+
assert_equal 'echostr', last_response.body
|
60
|
+
end
|
61
|
+
|
62
|
+
test 'POST configured path' do
|
63
|
+
encrypt = cryptor.encrypt('<response>secret</response>')
|
64
|
+
signature = signer.sign(1, 2, encrypt)
|
65
|
+
post "/wechat?timestamp=1&nonce=2&msg_signature=#{signature}", "<xml><Encrypt>#{encrypt}</Encrypt></xml>"
|
66
|
+
response = MultiXml.parse(last_response.body).fetch('xml')
|
67
|
+
assert_equal 'secret', cryptor.decrypt(response['Encrypt'])
|
68
|
+
assert_equal response['MsgSignature'], signer.sign(response['Encrypt'], response['Nonce'], response['TimeStamp'])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'xi_wechat_corp/pkcs7_encoder'
|
3
|
+
|
4
|
+
module XiWechatCorp
|
5
|
+
class PKCS7EncoderTest < Minitest::Test
|
6
|
+
def encoder
|
7
|
+
@encoder ||= XiWechatCorp::PKCS7Encoder.new
|
8
|
+
end
|
9
|
+
|
10
|
+
CASES = {
|
11
|
+
'' => 32.chr * 32,
|
12
|
+
'a' => 'a' + 31.chr * 31,
|
13
|
+
'a' * 31 => 'a' * 31 + 1.chr,
|
14
|
+
'a' * 32 => 'a' * 32 + 32.chr * 32,
|
15
|
+
}
|
16
|
+
|
17
|
+
def test_encode
|
18
|
+
CASES.each_pair do |raw, encoded|
|
19
|
+
assert_equal encoded, encoder.encode(raw)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_decode
|
24
|
+
CASES.each_pair do |raw, encoded|
|
25
|
+
assert_equal raw, encoder.decode(encoded)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'xi_wechat_corp/sha1_signer'
|
3
|
+
|
4
|
+
module XiWechatCorp
|
5
|
+
class SHA1SignerTest < Minitest::Test
|
6
|
+
TEST_TOKEN = 'QDG6eK'
|
7
|
+
TEST_TIME_STAMP = '1409659589'
|
8
|
+
TEST_NONCE = '263014780'
|
9
|
+
TEST_ECHO_STR = 'P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ=='
|
10
|
+
TEST_SIGNATURE = '5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3'
|
11
|
+
def signer
|
12
|
+
SHA1Signer.new(TEST_TOKEN)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_sign_example
|
16
|
+
assert_equal TEST_SIGNATURE, signer.sign(TEST_TIME_STAMP, TEST_NONCE, TEST_ECHO_STR)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'xi_wechat_corp/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'xi_wechat_corp'
|
8
|
+
spec.version = XiWechatCorp::VERSION
|
9
|
+
spec.authors = ['Ian Yang']
|
10
|
+
spec.email = ['ian@3pjgames.com']
|
11
|
+
spec.summary = %q{Wechat Corp development toolkits}
|
12
|
+
spec.description = %q{Wechat Corp development toolkits}
|
13
|
+
spec.homepage = 'https://github.com/3pjgames/xi_wechat_corp'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'faraday', '~> 0.9.1'
|
22
|
+
spec.add_dependency 'faraday_middleware', '~> 0.9.1'
|
23
|
+
spec.add_dependency 'multi_xml', '~> 0.5.5'
|
24
|
+
spec.add_dependency 'multi_json', '~> 1.10.1'
|
25
|
+
spec.add_dependency 'rack', '~> 1.6.0'
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
27
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
28
|
+
spec.add_development_dependency 'minitest', '~> 5.5.1'
|
29
|
+
spec.add_development_dependency 'webmock'
|
30
|
+
spec.add_development_dependency 'rack-test'
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xi_wechat_corp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ian Yang
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faraday
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.9.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.9.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday_middleware
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.9.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.9.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: multi_xml
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.5.5
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.5.5
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: multi_json
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.10.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.10.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.6.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.6.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: bundler
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.7'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.7'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '10.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '10.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: minitest
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 5.5.1
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 5.5.1
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: webmock
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rack-test
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description: Wechat Corp development toolkits
|
154
|
+
email:
|
155
|
+
- ian@3pjgames.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- ".gitignore"
|
161
|
+
- Gemfile
|
162
|
+
- LICENSE.txt
|
163
|
+
- README.md
|
164
|
+
- Rakefile
|
165
|
+
- lib/xi_wechat_corp.rb
|
166
|
+
- lib/xi_wechat_corp/aes_crypt.rb
|
167
|
+
- lib/xi_wechat_corp/api.rb
|
168
|
+
- lib/xi_wechat_corp/api/access_token.rb
|
169
|
+
- lib/xi_wechat_corp/api/connection.rb
|
170
|
+
- lib/xi_wechat_corp/callback.rb
|
171
|
+
- lib/xi_wechat_corp/callback/config.rb
|
172
|
+
- lib/xi_wechat_corp/callback/rack.rb
|
173
|
+
- lib/xi_wechat_corp/callback/request.rb
|
174
|
+
- lib/xi_wechat_corp/callback/response.rb
|
175
|
+
- lib/xi_wechat_corp/error.rb
|
176
|
+
- lib/xi_wechat_corp/pkcs7_encoder.rb
|
177
|
+
- lib/xi_wechat_corp/sha1_signer.rb
|
178
|
+
- lib/xi_wechat_corp/version.rb
|
179
|
+
- test/test_helper.rb
|
180
|
+
- test/xi_wechat_corp/aes_crypt_test.rb
|
181
|
+
- test/xi_wechat_corp/api/access_token_test.rb
|
182
|
+
- test/xi_wechat_corp/api/connection_test.rb
|
183
|
+
- test/xi_wechat_corp/callback/rack_test.rb
|
184
|
+
- test/xi_wechat_corp/pkcs7_encoder_test.rb
|
185
|
+
- test/xi_wechat_corp/sha1_signer_test.rb
|
186
|
+
- xi_wechat_corp.gemspec
|
187
|
+
homepage: https://github.com/3pjgames/xi_wechat_corp
|
188
|
+
licenses:
|
189
|
+
- MIT
|
190
|
+
metadata: {}
|
191
|
+
post_install_message:
|
192
|
+
rdoc_options: []
|
193
|
+
require_paths:
|
194
|
+
- lib
|
195
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - ">="
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: '0'
|
200
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
201
|
+
requirements:
|
202
|
+
- - ">="
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
version: '0'
|
205
|
+
requirements: []
|
206
|
+
rubyforge_project:
|
207
|
+
rubygems_version: 2.2.2
|
208
|
+
signing_key:
|
209
|
+
specification_version: 4
|
210
|
+
summary: Wechat Corp development toolkits
|
211
|
+
test_files:
|
212
|
+
- test/test_helper.rb
|
213
|
+
- test/xi_wechat_corp/aes_crypt_test.rb
|
214
|
+
- test/xi_wechat_corp/api/access_token_test.rb
|
215
|
+
- test/xi_wechat_corp/api/connection_test.rb
|
216
|
+
- test/xi_wechat_corp/callback/rack_test.rb
|
217
|
+
- test/xi_wechat_corp/pkcs7_encoder_test.rb
|
218
|
+
- test/xi_wechat_corp/sha1_signer_test.rb
|