iapserver 0.1.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/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: []
|