iapserver 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/assets/AppleRootCA-G3.cer +0 -0
- data/bin/iapserver +4 -0
- data/lib/iap/appstore_config.rb +71 -0
- data/lib/iap/config.rb +84 -0
- data/lib/iap/history.rb +54 -0
- data/lib/iap/jwttools.rb +85 -0
- data/lib/iap/notification_history.rb +65 -0
- data/lib/iap/order.rb +60 -0
- data/lib/iap/receipt.rb +85 -0
- data/lib/iap/refund.rb +54 -0
- data/lib/iap/request.rb +31 -0
- data/lib/iap/subscription.rb +40 -0
- data/lib/iap/transaction.rb +54 -0
- data/lib/iapserver.rb +121 -0
- data/lib/version.rb +4 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2a403c1d622c91998cf08f8a4b1c6169e7db207be81ba949be96dd277e280f2a
|
4
|
+
data.tar.gz: f5fea2ddc0e141f902787776eda279b9ea0c7484f9523aa5663b5a14170ecdbd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 84ac3c0c6db814620aa80a590bcc58ddcf0ad428bf1400374e77e7d6fabcb0ee0607af07b91a63446f3ad273a92cbe1b234a50464165d15fae9f6eb5c84d675d
|
7
|
+
data.tar.gz: e455044d03126adbf7f609bf88a2c55a33a7bd552b7a9c8911a4382b4398c6f3301a169582bd5fb9de817b82d3b6cf478ce8e94054092b9e6f1ff9f637169378
|
Binary file
|
data/bin/iapserver
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module IAPServer
|
5
|
+
class AppStoreConfig
|
6
|
+
def self.config_path
|
7
|
+
@config_path ||= begin
|
8
|
+
path = Pathname.new(File.expand_path('~/.iapserver_config'))
|
9
|
+
path.mkpath unless path.exist?
|
10
|
+
path
|
11
|
+
end
|
12
|
+
@config_path
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.default_config
|
16
|
+
configs = all_configs
|
17
|
+
return configs.first if configs.size == 1
|
18
|
+
raise "你还没有配置AppStore Connect的密钥参数,执行`iapserver config -a 'xxx'`添加。".red if configs.size == 0
|
19
|
+
|
20
|
+
list_configs
|
21
|
+
num = input("有多个配置,请输入序号选择合适的配置:")
|
22
|
+
raise "序号输入有误".red if num.empty?
|
23
|
+
|
24
|
+
raise "序号输入有误".red unless (0...configs.size) === num.to_i - 1
|
25
|
+
return configs[(num.to_i - 1)]
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.input(message)
|
29
|
+
print "#{message}".red
|
30
|
+
STDIN.gets.chomp.strip
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.all_configs
|
34
|
+
config_path.children().map do |child|
|
35
|
+
next if File.basename(child) == '.DS_Store'
|
36
|
+
next unless File.file?(child)
|
37
|
+
JSON.parse(File.read(child))
|
38
|
+
end.reject(&:nil?) || []
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.add_config(add_name, config)
|
42
|
+
add_path = config_path + add_name
|
43
|
+
json = config.to_json
|
44
|
+
File.write(add_path, json)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.list_configs(show_more=false)
|
48
|
+
configs = all_configs
|
49
|
+
index = 1
|
50
|
+
configs.each do |config|
|
51
|
+
name, key, kid, iss, bid = config['name'], config['key'], config['kid'], config['iss'], config['bid']
|
52
|
+
puts "#{index}) #{name}"
|
53
|
+
puts " key: #{key}" if show_more
|
54
|
+
puts " kid: #{kid}" if show_more
|
55
|
+
puts " iss: #{iss}" if show_more
|
56
|
+
puts " bid: #{bid}" if show_more
|
57
|
+
index += 1
|
58
|
+
end
|
59
|
+
return configs
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.clear_all_configs
|
63
|
+
config_path.rmtree if config_path.exist?
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.clear_config(name)
|
67
|
+
path = config_path + name
|
68
|
+
path.rmtree if path.exist?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/iap/config.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require_relative 'appstore_config'
|
2
|
+
|
3
|
+
module IAPServer
|
4
|
+
class Config
|
5
|
+
def run(options, args)
|
6
|
+
# get the command line inputs and parse those into the vars we need...
|
7
|
+
configs, add_name, detail, remove = get_inputs(options, args)
|
8
|
+
|
9
|
+
if configs && configs.size > 0
|
10
|
+
raise "路径不存在!".red unless File.exist? configs
|
11
|
+
begin
|
12
|
+
json = JSON.parse(File.read(configs))
|
13
|
+
raise "读取的json不是一个数组!" unless json.is_a?(Array)
|
14
|
+
json.each do |config|
|
15
|
+
add_name = config['name']
|
16
|
+
next if add_name.nil? || add_name.empty?
|
17
|
+
add_config(add_name, config)
|
18
|
+
end
|
19
|
+
rescue StandardError => e
|
20
|
+
raise "json读取错误,请检查配置文件!".red
|
21
|
+
end
|
22
|
+
elsif add_name
|
23
|
+
strip_name = add_name.strip
|
24
|
+
raise "配置名称不能为空!".red if strip_name.empty?
|
25
|
+
add_config(strip_name)
|
26
|
+
else
|
27
|
+
# list all configs
|
28
|
+
configs = IAPServer::AppStoreConfig.list_configs(detail)
|
29
|
+
if configs.size > 0
|
30
|
+
return unless remove
|
31
|
+
all_num = "#{configs.size + 1}"
|
32
|
+
puts "#{all_num}) 删除所有配置".red
|
33
|
+
num = self.input("请输入前面的序号:")
|
34
|
+
return if num.empty? || num.to_i <= 0
|
35
|
+
|
36
|
+
if num == all_num
|
37
|
+
IAPServer::AppStoreConfig.clear_all_configs
|
38
|
+
elsif num.to_i > 0 && configs.size > (num.to_i - 1)
|
39
|
+
config = configs[(num.to_i - 1)]
|
40
|
+
name = config['name']
|
41
|
+
IAPServer::AppStoreConfig.clear_config(name)
|
42
|
+
else
|
43
|
+
puts "参数输入错误,退出"
|
44
|
+
end
|
45
|
+
else
|
46
|
+
puts "你还没有配置AppStore Connect的密钥和相关参数"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_inputs(options, args)
|
52
|
+
configs = options.config
|
53
|
+
add_name = options.add
|
54
|
+
detail = options.detail
|
55
|
+
remove = options.remove
|
56
|
+
|
57
|
+
return configs, add_name, detail, remove
|
58
|
+
end
|
59
|
+
|
60
|
+
def input(message)
|
61
|
+
print "#{message}".red
|
62
|
+
STDIN.gets.chomp.strip
|
63
|
+
end
|
64
|
+
|
65
|
+
def add_config(add_name, dic={})
|
66
|
+
key = dic['key'] || self.input('Put AppStore key: ')
|
67
|
+
kid = dic['kid'] || self.input('Put AppStore key id: ')
|
68
|
+
iss = dic['iss'] || self.input('Put AppStore issuser id: ')
|
69
|
+
bid = dic['bid'] || self.input('Put AppStore bundleid: ')
|
70
|
+
raise "输入的参数有误,添加失败!".red if key.empty? or kid.empty? or iss.empty? or bid.empty?
|
71
|
+
|
72
|
+
# fix key
|
73
|
+
key = key.split("\\n").join("\n") if key.include?("\\n")
|
74
|
+
config = {
|
75
|
+
'name': add_name,
|
76
|
+
'key': key,
|
77
|
+
'kid': kid,
|
78
|
+
'iss': iss,
|
79
|
+
'bid': bid
|
80
|
+
}
|
81
|
+
IAPServer::AppStoreConfig.add_config(add_name, config)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/iap/history.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require "json"
|
2
|
+
require_relative 'jwttools.rb'
|
3
|
+
require_relative 'request.rb'
|
4
|
+
|
5
|
+
module IAPServer
|
6
|
+
class History
|
7
|
+
def run(options, args)
|
8
|
+
# get the command line inputs and parse those into the vars we need...
|
9
|
+
transaction_id = get_inputs(options, args)
|
10
|
+
if transaction_id.nil? || transaction_id.empty?
|
11
|
+
raise "必须有transaction id。".red
|
12
|
+
end
|
13
|
+
|
14
|
+
req = IAPServer::StoreRequest.new :sandbox => options.sandbox
|
15
|
+
resp = req.request("/inApps/v1/history/#{transaction_id}", IAPServer::JWTTools.generate)
|
16
|
+
validation_jwt(resp)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_inputs(options, args)
|
20
|
+
transaction_id = args.first || self.input('put transaction id: ')
|
21
|
+
return transaction_id
|
22
|
+
end
|
23
|
+
|
24
|
+
def input(message)
|
25
|
+
print "#{message}".red
|
26
|
+
STDIN.gets.chomp.strip
|
27
|
+
end
|
28
|
+
|
29
|
+
# 验证jwt
|
30
|
+
def validation_jwt(resp)
|
31
|
+
puts 'Code = ' + resp.code ##请求状态码
|
32
|
+
puts 'Message = ' + resp.message
|
33
|
+
if resp.code == '200'
|
34
|
+
body = JSON.parse(resp.body)
|
35
|
+
|
36
|
+
jwt_list = body["signedTransactions"]
|
37
|
+
if !jwt_list.nil? && jwt_list.count > 0
|
38
|
+
jwt_token = jwt_list[0]
|
39
|
+
|
40
|
+
if IAPServer::JWTTools.good_signature(jwt_token)
|
41
|
+
payload = IAPServer::JWTTools.payload(jwt_token)
|
42
|
+
puts "Payload:" + payload
|
43
|
+
else
|
44
|
+
puts "JWT 验证失败"
|
45
|
+
end
|
46
|
+
else
|
47
|
+
puts '没有查询到历史订单'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
data/lib/iap/jwttools.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require "uuidtools"
|
2
|
+
require 'jwt'
|
3
|
+
require 'openssl'
|
4
|
+
require 'json'
|
5
|
+
require_relative 'appstore_config'
|
6
|
+
|
7
|
+
module IAPServer
|
8
|
+
class JWTTools
|
9
|
+
def self.generate
|
10
|
+
config = IAPServer::AppStoreConfig.default_config
|
11
|
+
raise "获取秘钥信息失败,请检查。" if config.nil?
|
12
|
+
|
13
|
+
key, kid, iss, bid = config['key'], config['kid'], config['iss'], config['bid']
|
14
|
+
# fix key
|
15
|
+
key = key.split("\\n").join("\n") if key.include?("\\n")
|
16
|
+
generate_token(key, kid, iss, bid)
|
17
|
+
end
|
18
|
+
|
19
|
+
# 秘钥串、秘钥ID、Issuer ID、bundle id
|
20
|
+
def self.generate_token(key, kid, iss, bid)
|
21
|
+
# JWT Header
|
22
|
+
header = {
|
23
|
+
"alg": "ES256", # 固定值
|
24
|
+
"kid": kid, # private key ID from App Store Connect
|
25
|
+
"typ": "JWT" # 固定值
|
26
|
+
}
|
27
|
+
|
28
|
+
iat = Time.new
|
29
|
+
# JWT Payload
|
30
|
+
payload = {
|
31
|
+
"iss": iss, # Your issuer ID from the Keys page in App Store Connect
|
32
|
+
"aud": "appstoreconnect-v1", # 固定值
|
33
|
+
"iat": iat.to_i, # 令牌生成时间,UNIX时间单位,秒
|
34
|
+
"exp": iat.to_i + 60 * 60, # 令牌失效时间,60 minutes timestamp
|
35
|
+
"nonce": UUIDTools::UUID.timestamp_create.to_s, # An arbitrary number you create and use only once
|
36
|
+
"bid": bid # Your app's bundle ID
|
37
|
+
}
|
38
|
+
|
39
|
+
ecdsa_key = OpenSSL::PKey::EC.new key
|
40
|
+
# JWT token
|
41
|
+
token = JWT.encode payload, ecdsa_key, algorithm='ES256', header_fields=header
|
42
|
+
token
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.good_signature(jws_token)
|
46
|
+
realpath = File.expand_path("#{File.dirname(__FILE__)}/../../assets/AppleRootCA-G3.cer")
|
47
|
+
raw = File.read "#{realpath}"
|
48
|
+
apple_root_cert = OpenSSL::X509::Certificate.new(raw)
|
49
|
+
|
50
|
+
parts = jws_token.split(".")
|
51
|
+
decoded_parts = parts.map { |part| Base64.decode64(part) }
|
52
|
+
header = JSON.parse(decoded_parts[0])
|
53
|
+
# puts "Header:#{decoded_parts[0]}"
|
54
|
+
# puts "Payload:#{decoded_parts[1]}"
|
55
|
+
|
56
|
+
cert_chain = header["x5c"].map { |part| OpenSSL::X509::Certificate.new(Base64.decode64(part))}
|
57
|
+
return false unless cert_chain.last == apple_root_cert
|
58
|
+
|
59
|
+
for n in 0..(cert_chain.count - 2)
|
60
|
+
return false unless cert_chain[n].verify(cert_chain[n+1].public_key)
|
61
|
+
end
|
62
|
+
|
63
|
+
begin
|
64
|
+
decoded_token = JWT.decode(jws_token, cert_chain[0].public_key, true, { algorithms: ['ES256'] })
|
65
|
+
!decoded_token.nil?
|
66
|
+
rescue JWT::JWKError
|
67
|
+
false
|
68
|
+
rescue JWT::DecodeError
|
69
|
+
false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.payload(jws_token)
|
74
|
+
parts = jws_token.split(".")
|
75
|
+
decoded_parts = parts.map { |part| Base64.decode64(part) }
|
76
|
+
decoded_parts[1]
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.header(jws_token)
|
80
|
+
parts = jws_token.split(".")
|
81
|
+
decoded_parts = parts.map { |part| Base64.decode64(part) }
|
82
|
+
decoded_parts[0]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "json"
|
2
|
+
require_relative 'jwttools.rb'
|
3
|
+
require_relative 'request.rb'
|
4
|
+
|
5
|
+
module IAPServer
|
6
|
+
class NotificationHistory
|
7
|
+
def run(options, args)
|
8
|
+
transaction_id = args.first
|
9
|
+
lastdays = options.lastdays
|
10
|
+
only_failures = options.only_failures
|
11
|
+
notification_type = options.noti_type
|
12
|
+
notification_subtype = options.noti_subtype
|
13
|
+
query_params = begin
|
14
|
+
query = ''
|
15
|
+
query = "paginationToken=#{options.token}" if options.token
|
16
|
+
query
|
17
|
+
end
|
18
|
+
|
19
|
+
raise "查询过去天数区间:[1-180]".red unless (1..180) === lastdays
|
20
|
+
|
21
|
+
end_time = Time.now.utc.to_i #s
|
22
|
+
start_time = end_time - (60 * 60 * 24 * lastdays)
|
23
|
+
|
24
|
+
params = {
|
25
|
+
"startDate": (start_time * 1000).to_s,
|
26
|
+
"endDate": (end_time * 1000).to_s,
|
27
|
+
'onlyFailures': only_failures
|
28
|
+
}
|
29
|
+
params['notificationType'] = notification_type unless notification_type.nil?
|
30
|
+
params['notificationSubtype'] = notification_subtype unless notification_subtype.nil?
|
31
|
+
params['transactionId'] = transaction_id unless transaction_id.nil?
|
32
|
+
|
33
|
+
req = IAPServer::StoreRequest.new :use_post => true, :sandbox => options.sandbox
|
34
|
+
resp = req.request("/inApps/v1/notifications/history/?#{query_params}", IAPServer::JWTTools.generate, params)
|
35
|
+
validation_jwt(resp)
|
36
|
+
end
|
37
|
+
|
38
|
+
# 验证jwt
|
39
|
+
def validation_jwt(resp)
|
40
|
+
puts 'Code = ' + resp.code ##请求状态码
|
41
|
+
puts 'Message = ' + resp.message
|
42
|
+
if resp.code == '200'
|
43
|
+
body = JSON.parse(resp.body)
|
44
|
+
notificationHistory = body["notificationHistory"]
|
45
|
+
|
46
|
+
index = 1
|
47
|
+
notificationHistory.each do |history|
|
48
|
+
signedPayload = history['signedPayload']
|
49
|
+
payload = IAPServer::JWTTools.payload(signedPayload)
|
50
|
+
|
51
|
+
payload_json = JSON.parse(payload)
|
52
|
+
signedTransactionInfo = payload_json['data']['signedTransactionInfo']
|
53
|
+
payload_json['data']['signedTransactionInfo'] = IAPServer::JWTTools.payload(signedTransactionInfo)
|
54
|
+
puts "#{index})".red
|
55
|
+
puts payload_json.to_json
|
56
|
+
index += 1
|
57
|
+
end
|
58
|
+
|
59
|
+
puts "还有更多的历史通知,继续查询可以传token:#{body["paginationToken"]}".red if body["hasMore"]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
data/lib/iap/order.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require "json"
|
2
|
+
require_relative './jwttools.rb'
|
3
|
+
require_relative './request.rb'
|
4
|
+
|
5
|
+
module IAPServer
|
6
|
+
class Order
|
7
|
+
def run(options, args)
|
8
|
+
# get the command line inputs and parse those into the vars we need...
|
9
|
+
order_num = get_inputs(options, args)
|
10
|
+
raise "必须有订单号。".red if order_num.nil? || order_num.empty?
|
11
|
+
|
12
|
+
req = IAPServer::StoreRequest.new
|
13
|
+
resp = req.request("/inApps/v1/lookup/#{order_num}", IAPServer::JWTTools.generate)
|
14
|
+
validation_jwt(resp)
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_inputs(options, args)
|
18
|
+
order_num = args.first || self.input('put order-num: ')
|
19
|
+
|
20
|
+
return order_num
|
21
|
+
end
|
22
|
+
|
23
|
+
def input(message)
|
24
|
+
print "#{message}".red
|
25
|
+
STDIN.gets.chomp.strip
|
26
|
+
end
|
27
|
+
|
28
|
+
# 验证jwt
|
29
|
+
def validation_jwt(resp)
|
30
|
+
puts 'Code = ' + resp.code ##请求状态码
|
31
|
+
puts 'Message = ' + resp.message
|
32
|
+
if resp.code == '200'
|
33
|
+
body = JSON.parse(resp.body)
|
34
|
+
|
35
|
+
status = body["status"]
|
36
|
+
if !status.nil? && status == 0
|
37
|
+
jwt_list = body["signedTransactions"]
|
38
|
+
jwt_token = jwt_list[0]
|
39
|
+
|
40
|
+
if IAPServer::JWTTools.good_signature(jwt_token)
|
41
|
+
payload = IAPServer::JWTTools.payload(jwt_token)
|
42
|
+
puts "Payload:" + payload
|
43
|
+
|
44
|
+
result = JSON.parse(payload)
|
45
|
+
purchaseDate = result["purchaseDate"]
|
46
|
+
puts "支付时间:#{Time.at(purchaseDate/1000)}"
|
47
|
+
else
|
48
|
+
puts "JWT 验证失败"
|
49
|
+
end
|
50
|
+
elsif status == 1
|
51
|
+
puts "status:#{status},查询失败。可能的原因:jwt签名正确,bundleid不一致"
|
52
|
+
else
|
53
|
+
puts "status:#{status},请排查错误原因"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
data/lib/iap/receipt.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module IAPServer
|
5
|
+
class Receipt
|
6
|
+
def run(options, args)
|
7
|
+
# get the command line inputs and parse those into the vars we need...
|
8
|
+
receipt_data, sandbox, password = get_inputs(options, args)
|
9
|
+
raise "必须有票据才能验证。".red if receipt_data.nil? || receipt_data.empty?
|
10
|
+
|
11
|
+
verify(receipt_data, sandbox, password)
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_inputs(options, args)
|
15
|
+
receipt_data = args.first || begin
|
16
|
+
file = options.file || ''
|
17
|
+
File.read(file) if File.exist?(file) && File.file?(file)
|
18
|
+
rescue
|
19
|
+
raise "文件读取失败".red
|
20
|
+
end
|
21
|
+
if receipt_data.nil?
|
22
|
+
path = self.input('Path to receipt-data file: ')
|
23
|
+
receipt_data = File.read(path).chomp if File.exist? path
|
24
|
+
end
|
25
|
+
sandbox = options.sandbox
|
26
|
+
password = options.password
|
27
|
+
|
28
|
+
return receipt_data, sandbox, password
|
29
|
+
end
|
30
|
+
|
31
|
+
def input(message)
|
32
|
+
print "#{message}".red
|
33
|
+
STDIN.gets.chomp.strip
|
34
|
+
end
|
35
|
+
|
36
|
+
def request(receipt, sandbox, password)
|
37
|
+
uri = sandbox ? 'sandbox.itunes.apple.com' : 'buy.itunes.apple.com'
|
38
|
+
http = Net::HTTP.new(uri, 443)
|
39
|
+
http.use_ssl = true
|
40
|
+
|
41
|
+
headers = { ##定义http请求头信息
|
42
|
+
'Content-Type' => 'application/json'
|
43
|
+
}
|
44
|
+
params = {
|
45
|
+
'receipt-data' => "#{receipt}",
|
46
|
+
'exclude-old-transactions' => true,
|
47
|
+
}
|
48
|
+
params['password'] = password unless password.nil?
|
49
|
+
resp = http.post("/verifyReceipt", params.to_json, headers)
|
50
|
+
resp
|
51
|
+
end
|
52
|
+
|
53
|
+
def verify(receipt, sandbox=false, password=nil)
|
54
|
+
resp = request(receipt, sandbox, password)
|
55
|
+
if resp.code == '200'
|
56
|
+
body = JSON.parse(resp.body)
|
57
|
+
status = body['status']
|
58
|
+
if status == 0
|
59
|
+
puts resp.body
|
60
|
+
else
|
61
|
+
error_msg = status_declare(status)
|
62
|
+
puts "status: #{status}, #{error_msg}"
|
63
|
+
end
|
64
|
+
else
|
65
|
+
puts "Code: #{resp.code}"
|
66
|
+
puts "Message: #{resp.message}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def status_declare(status)
|
71
|
+
error_status = {
|
72
|
+
'21000' => 'App Store无法读取你提供的JSON数据',
|
73
|
+
'21002' => '收据数据不符合格式',
|
74
|
+
'21003' => '收据无法被验证',
|
75
|
+
'21004' => '你提供的共享密钥和账户的共享密钥不一致',
|
76
|
+
'21005' => '收据服务器当前不可用',
|
77
|
+
'21006' => '收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中',
|
78
|
+
'21007' => '收据信息是测试用(sandbox),但却被发送到产品环境中验证',
|
79
|
+
'21008' => '收据信息是产品环境中使用,但却被发送到测试环境中验证',
|
80
|
+
}
|
81
|
+
error_msg = error_status[status.to_s] || "未知的status类型,请对照https://developer.apple.com/documentation/appstorereceipts/status排查"
|
82
|
+
error_msg
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/iap/refund.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require "json"
|
2
|
+
require_relative 'jwttools.rb'
|
3
|
+
require_relative 'request.rb'
|
4
|
+
|
5
|
+
module IAPServer
|
6
|
+
class Refund
|
7
|
+
def run(options, args)
|
8
|
+
# get the command line inputs and parse those into the vars we need...
|
9
|
+
transaction_id = get_inputs(options, args)
|
10
|
+
if transaction_id.nil? || transaction_id.empty?
|
11
|
+
raise "必须有transaction id。".red
|
12
|
+
end
|
13
|
+
|
14
|
+
req = IAPServer::StoreRequest.new :sandbox => options.sandbox
|
15
|
+
resp = req.request("/inApps/v1/refund/lookup/#{transaction_id}", IAPServer::JWTTools.generate)
|
16
|
+
validation_jwt(resp)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_inputs(options, args)
|
20
|
+
transaction_id = args.first || self.input('put transaction id: ')
|
21
|
+
return transaction_id
|
22
|
+
end
|
23
|
+
|
24
|
+
def input(message)
|
25
|
+
print "#{message}".red
|
26
|
+
STDIN.gets.chomp.strip
|
27
|
+
end
|
28
|
+
|
29
|
+
# 验证jwt
|
30
|
+
def validation_jwt(resp)
|
31
|
+
puts 'Code = ' + resp.code ##请求状态码
|
32
|
+
puts 'Message = ' + resp.message
|
33
|
+
if resp.code == '200'
|
34
|
+
body = JSON.parse(resp.body)
|
35
|
+
|
36
|
+
jwt_list = body["signedTransactions"]
|
37
|
+
if !jwt_list.nil? && jwt_list.count > 0
|
38
|
+
jwt_token = jwt_list[0]
|
39
|
+
|
40
|
+
if IAPServer::JWTTools.good_signature(jwt_token)
|
41
|
+
payload = IAPServer::JWTTools.payload(jwt_token)
|
42
|
+
puts "Payload:" + payload
|
43
|
+
else
|
44
|
+
puts "JWT 验证失败"
|
45
|
+
end
|
46
|
+
else
|
47
|
+
puts '没有查询到退款信息'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
data/lib/iap/request.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module IAPServer
|
5
|
+
class StoreRequest
|
6
|
+
def initialize(use_post: false, sandbox: false)
|
7
|
+
@use_post = use_post
|
8
|
+
@sandbox = sandbox
|
9
|
+
end
|
10
|
+
|
11
|
+
def request(path, token, params={})
|
12
|
+
http = Net::HTTP.new('api.storekit.itunes.apple.com', 443)
|
13
|
+
if @sandbox
|
14
|
+
http = Net::HTTP.new('api.storekit-sandbox.itunes.apple.com', 443)
|
15
|
+
end
|
16
|
+
http.use_ssl = true
|
17
|
+
|
18
|
+
headers = { ##定义http请求头信息
|
19
|
+
'Authorization' => "Bearer #{token}",
|
20
|
+
'Content-Type' => 'application/json'
|
21
|
+
}
|
22
|
+
if @use_post
|
23
|
+
resp = http.post(path, params.to_json, headers)
|
24
|
+
else
|
25
|
+
resp = http.get(path, headers)
|
26
|
+
end
|
27
|
+
resp
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "json"
|
2
|
+
require_relative 'jwttools.rb'
|
3
|
+
require_relative 'request.rb'
|
4
|
+
|
5
|
+
module IAPServer
|
6
|
+
class Subscription
|
7
|
+
def run(options, args)
|
8
|
+
# get the command line inputs and parse those into the vars we need...
|
9
|
+
transaction_id = get_inputs(options, args)
|
10
|
+
if transaction_id.nil? || transaction_id.empty?
|
11
|
+
raise "必须有transaction id。".red
|
12
|
+
end
|
13
|
+
|
14
|
+
req = IAPServer::StoreRequest.new :sandbox => options.sandbox
|
15
|
+
resp = req.request("/inApps/v1/subscriptions/#{transaction_id}", IAPServer::JWTTools.generate)
|
16
|
+
validation_jwt(resp)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_inputs(options, args)
|
20
|
+
transaction_id = args.first || self.input('put transaction id: ')
|
21
|
+
return transaction_id
|
22
|
+
end
|
23
|
+
|
24
|
+
def input(message)
|
25
|
+
print "#{message}".red
|
26
|
+
STDIN.gets.chomp.strip
|
27
|
+
end
|
28
|
+
|
29
|
+
# 验证jwt
|
30
|
+
def validation_jwt(resp)
|
31
|
+
puts 'Code = ' + resp.code ##请求状态码
|
32
|
+
puts 'Message = ' + resp.message
|
33
|
+
if resp.code == '200'
|
34
|
+
puts 'Body: ' + resp.body
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "json"
|
2
|
+
require_relative 'jwttools.rb'
|
3
|
+
require_relative 'request.rb'
|
4
|
+
|
5
|
+
module IAPServer
|
6
|
+
class Transaction
|
7
|
+
def run(options, args)
|
8
|
+
# get the command line inputs and parse those into the vars we need...
|
9
|
+
transaction_id = get_inputs(options, args)
|
10
|
+
if transaction_id.nil? || transaction_id.empty?
|
11
|
+
raise "必须有transaction id。".red
|
12
|
+
end
|
13
|
+
|
14
|
+
req = IAPServer::StoreRequest.new :sandbox => options.sandbox
|
15
|
+
resp = req.request("/inApps/v1/transactions/#{transaction_id}", IAPServer::JWTTools.generate)
|
16
|
+
validation_jwt(resp)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_inputs(options, args)
|
20
|
+
transaction_id = args.first || self.input('put transaction id: ')
|
21
|
+
return transaction_id
|
22
|
+
end
|
23
|
+
|
24
|
+
def input(message)
|
25
|
+
print "#{message}".red
|
26
|
+
STDIN.gets.chomp.strip
|
27
|
+
end
|
28
|
+
|
29
|
+
# 验证jwt
|
30
|
+
def validation_jwt(resp)
|
31
|
+
puts 'Code = ' + resp.code ##请求状态码
|
32
|
+
puts 'Message = ' + resp.message
|
33
|
+
if resp.code == '200'
|
34
|
+
body = JSON.parse(resp.body)
|
35
|
+
|
36
|
+
jwt_list = body["signedTransactions"]
|
37
|
+
if !jwt_list.nil? && jwt_list.count > 0
|
38
|
+
jwt_token = jwt_list[0]
|
39
|
+
|
40
|
+
if IAPServer::JWTTools.good_signature(jwt_token)
|
41
|
+
payload = IAPServer::JWTTools.payload(jwt_token)
|
42
|
+
puts "Payload:" + payload
|
43
|
+
else
|
44
|
+
puts "JWT 验证失败"
|
45
|
+
end
|
46
|
+
else
|
47
|
+
puts '没有查询到交易信息'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
data/lib/iapserver.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'commander/import'
|
3
|
+
|
4
|
+
require 'colored2'
|
5
|
+
require 'version'
|
6
|
+
|
7
|
+
require 'iap/receipt'
|
8
|
+
require 'iap/order'
|
9
|
+
require 'iap/config'
|
10
|
+
require 'iap/history'
|
11
|
+
require 'iap/transaction'
|
12
|
+
require 'iap/subscription'
|
13
|
+
require 'iap/refund'
|
14
|
+
require 'iap/notification_history'
|
15
|
+
|
16
|
+
module IAPServer
|
17
|
+
class CommandsGenerator
|
18
|
+
|
19
|
+
def self.start
|
20
|
+
self.new.run
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
program :name, 'iapserver'
|
25
|
+
program :version, IAPServer::VERSION
|
26
|
+
program :description, '查询苹果iap交易信息和票据认证信息'
|
27
|
+
|
28
|
+
global_option('--sandbox', '是否为Sandbox环境,默认为Product环境。') { $sandbox = false }
|
29
|
+
# default_command :receipt
|
30
|
+
|
31
|
+
command :receipt do |c|
|
32
|
+
c.syntax = 'iapserver receipt args [options]'
|
33
|
+
c.description = 'iap票据验证'
|
34
|
+
c.option '-p', '--password STRING', String, '共享密钥'
|
35
|
+
c.option '-f', '--file FILE', String, 'Base64后的票据存放路径,格式:文本。'
|
36
|
+
c.action do |args, options|
|
37
|
+
IAPServer::Receipt.new.run(options, args)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
command :order do |c|
|
42
|
+
c.syntax = 'iapserver order [order_id]'
|
43
|
+
c.description = 'iap订单查询'
|
44
|
+
c.action do |args, options|
|
45
|
+
IAPServer::Order.new.run(options, args)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
command :history do |c|
|
50
|
+
c.syntax = 'iapserver history [transaction_id]'
|
51
|
+
c.description = 'iap历史交易查询'
|
52
|
+
c.action do |args, options|
|
53
|
+
IAPServer::History.new.run(options, args)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
command :transaction do |c|
|
58
|
+
c.syntax = 'iapserver transaction [transaction_id]'
|
59
|
+
c.description = 'iap交易详情'
|
60
|
+
c.action do |args, options|
|
61
|
+
IAPServer::Transaction.new.run(options, args)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
command :subscription do |c|
|
66
|
+
c.syntax = 'iapserver subscription [transaction_id]'
|
67
|
+
c.description = 'iap订阅信息'
|
68
|
+
c.action do |args, options|
|
69
|
+
IAPServer::Subscription.new.run(options, args)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
command :refund do |c|
|
74
|
+
c.syntax = 'iapserver refund [transaction_id]'
|
75
|
+
c.description = 'iap退款信息'
|
76
|
+
c.action do |args, options|
|
77
|
+
IAPServer::Refund.new.run(options, args)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
command :noti_history do |c|
|
82
|
+
c.syntax = 'iapserver noti_history [transaction_id]'
|
83
|
+
c.description = 'iap服务端通知'
|
84
|
+
c.option '--lastdays INTEGER', Integer, '查询过去多少天的服务端通知[1-180]。默认30天'
|
85
|
+
c.option '--only-failures', '仅请求未成功到达服务器的通知'
|
86
|
+
c.option '--noti-type STRING', String, '通知类型。参考:`https://developer.apple.com/documentation/appstoreservernotifications/notificationtype`'
|
87
|
+
c.option '--noti-subtype STRING', String, '参考:`https://developer.apple.com/documentation/appstoreservernotifications/subtype`'
|
88
|
+
c.option '--token STRING', String, 'paginationToken'
|
89
|
+
c.action do |args, options|
|
90
|
+
options.default :lastdays => 30, :only_failures => false
|
91
|
+
IAPServer::NotificationHistory.new.run(options, args)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
command :config do |c|
|
96
|
+
c.syntax = 'iapserver config [options]'
|
97
|
+
c.description = 'Apple Store Connect配置'
|
98
|
+
c.option '-d', '--detail', '列出Apple Store Connect秘钥配置详情'
|
99
|
+
c.option '-r', '--remove', '列出并选择删除配置的密钥'
|
100
|
+
c.option '-a', '--add NAME', String, '添加单个配置'
|
101
|
+
c.option '-c', '--config JSON PATH', String, 'Apple Store Connect配置。JSON: [{"name":"名称","key":"秘钥","kid":"秘钥ID","iss":"issuser id","bid":"bundle id"}]'
|
102
|
+
#, &multiple_values_option_proc(c, "config", &proc { |value| value.split('=', 2) })
|
103
|
+
c.action do |args, options|
|
104
|
+
IAPServer::Config.new.run(options, args)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def multiple_values_option_proc(command, name)
|
110
|
+
proc do |value|
|
111
|
+
value = yield(value) if block_given?
|
112
|
+
option = command.proxy_options.find { |opt| opt[0] == name } || []
|
113
|
+
values = option[1] || []
|
114
|
+
values << value
|
115
|
+
|
116
|
+
command.proxy_options.delete option
|
117
|
+
command.proxy_options << [name, values]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: iapserver
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cary
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-11-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: jwt
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.7'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: uuidtools
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.2.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.2.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: openssl
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pathname
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: fileutils
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: colored2
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.1'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: commander
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '4.6'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '4.6'
|
111
|
+
description: 苹果内购票据验证、苹果AppStore Connect API查询命令行工具
|
112
|
+
email:
|
113
|
+
- guojiashuang@live.com
|
114
|
+
executables:
|
115
|
+
- iapserver
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- assets/AppleRootCA-G3.cer
|
120
|
+
- bin/iapserver
|
121
|
+
- lib/iap/appstore_config.rb
|
122
|
+
- lib/iap/config.rb
|
123
|
+
- lib/iap/history.rb
|
124
|
+
- lib/iap/jwttools.rb
|
125
|
+
- lib/iap/notification_history.rb
|
126
|
+
- lib/iap/order.rb
|
127
|
+
- lib/iap/receipt.rb
|
128
|
+
- lib/iap/refund.rb
|
129
|
+
- lib/iap/request.rb
|
130
|
+
- lib/iap/subscription.rb
|
131
|
+
- lib/iap/transaction.rb
|
132
|
+
- lib/iapserver.rb
|
133
|
+
- lib/version.rb
|
134
|
+
homepage: https://github.com/CaryGo/iapserver
|
135
|
+
licenses:
|
136
|
+
- MIT
|
137
|
+
metadata: {}
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: 2.6.0
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubygems_version: 3.1.2
|
154
|
+
signing_key:
|
155
|
+
specification_version: 4
|
156
|
+
summary: 苹果内购票据验证、苹果AppStore Connect API查询命令行工具
|
157
|
+
test_files: []
|