iapserver 0.1.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
+ 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,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'iapserver.rb'
3
+
4
+ IAPServer::CommandsGenerator.start
@@ -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
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
@@ -0,0 +1,4 @@
1
+
2
+ module IAPServer
3
+ VERSION = "0.1.0"
4
+ end
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: []