xi_wechat_corp 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in xi_wechat_corp.gemspec
4
+ gemspec
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,9 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.pattern = 'test/**/*_test.rb'
7
+ end
8
+
9
+ task :default => [:test]
@@ -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,2 @@
1
+ require 'xi_wechat_corp/api/access_token'
2
+ require 'xi_wechat_corp/api/connection'
@@ -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,7 @@
1
+ module XiWechatCorp
2
+ module Callback
3
+ end
4
+ end
5
+
6
+ require 'xi_wechat_corp/callback/request'
7
+ require 'xi_wechat_corp/callback/response'
@@ -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,3 @@
1
+ module XiWechatCorp
2
+ VERSION = '1.0.0'
3
+ 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
@@ -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