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 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: []