motion-provisioning 0.0.1
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/.gitignore +10 -0
- data/.rspec +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +106 -0
- data/LICENSE.txt +46 -0
- data/README.md +207 -0
- data/Rakefile +71 -0
- data/bin/console +6 -0
- data/bin/setup +8 -0
- data/lib/motion-provisioning.rb +231 -0
- data/lib/motion-provisioning/application.rb +53 -0
- data/lib/motion-provisioning/certificate.rb +278 -0
- data/lib/motion-provisioning/mobileprovision.rb +84 -0
- data/lib/motion-provisioning/provisioning_profile.rb +136 -0
- data/lib/motion-provisioning/spaceship/free_portal_client.rb +123 -0
- data/lib/motion-provisioning/spaceship/portal_client.rb +34 -0
- data/lib/motion-provisioning/tasks.rb +14 -0
- data/lib/motion-provisioning/utils.rb +59 -0
- data/lib/motion-provisioning/version.rb +3 -0
- data/motion-provisioning.gemspec +29 -0
- metadata +192 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
module MotionProvisioning
|
2
|
+
# Represents a .mobileprobision file on disk
|
3
|
+
class MobileProvision
|
4
|
+
|
5
|
+
attr_accessor :hash, :enabled_services, :certificates
|
6
|
+
|
7
|
+
# @param path (String): Path to the .mobileprovision file
|
8
|
+
def initialize(path)
|
9
|
+
file = File.read(path)
|
10
|
+
start_index = file.index("<?xml")
|
11
|
+
end_index = file.index("</plist>") + 8
|
12
|
+
length = end_index - start_index
|
13
|
+
self.hash = Plist::parse_xml(file.slice(start_index, length))
|
14
|
+
self.certificates = []
|
15
|
+
self.enabled_services = []
|
16
|
+
|
17
|
+
entitlements_keys = hash['Entitlements'].keys
|
18
|
+
Service.constants.each do |constant_name|
|
19
|
+
service = Service.const_get(constant_name)
|
20
|
+
keys = service.mobileprovision_keys
|
21
|
+
if (keys - entitlements_keys).empty?
|
22
|
+
self.enabled_services << service
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
hash['DeveloperCertificates'].each do |certificate|
|
27
|
+
self.certificates << certificate.read
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def name
|
32
|
+
hash['Name']
|
33
|
+
end
|
34
|
+
|
35
|
+
def devices
|
36
|
+
hash['ProvisionedDevices'].map(&:downcase)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Checks wether the .mobileprovision file is valid by checking its
|
40
|
+
# expiration date, entitlements and certificates
|
41
|
+
# @param certificate (String): Path to the certificate file
|
42
|
+
# @param app_entitlements (Hash): A hash containing the app's entitlements
|
43
|
+
# @return Boolean
|
44
|
+
def valid?(certificate, app_entitlements)
|
45
|
+
return false if hash['ExpirationDate'] < DateTime.now
|
46
|
+
|
47
|
+
# entitlements = hash['Entitlements']
|
48
|
+
# # Remove entitlements that are not relevant for
|
49
|
+
# # Always true in development mobileprovision
|
50
|
+
# entitlements.delete('get-task-allow')
|
51
|
+
# # Always true in distribution mobileprovision
|
52
|
+
# entitlements.delete('beta-reports-active')
|
53
|
+
# entitlements.delete('application-identifier')
|
54
|
+
# entitlements.delete('com.apple.developer.team-identifier')
|
55
|
+
# # Always present, usually "$teamidentifier.*"
|
56
|
+
# entitlements.delete('keychain-access-groups')
|
57
|
+
# entitlements.delete('aps-environment')
|
58
|
+
|
59
|
+
# if app_entitlements != entitlements
|
60
|
+
# missing_in_app = entitlements.to_a - app_entitlements.to_a
|
61
|
+
# if missing_in_app.any?
|
62
|
+
# Utils.log("Error", "These entitlements are present in the provisioning profile but not in your app configuration:")
|
63
|
+
# puts missing_in_app
|
64
|
+
# end
|
65
|
+
|
66
|
+
# missing_in_profile = app_entitlements.to_a - entitlements.to_a
|
67
|
+
# if missing_in_profile.any?
|
68
|
+
# Utils.log("Error", "These entitlements are present in your app configuration but not in your provisioning profile:")
|
69
|
+
# puts missing_in_profile
|
70
|
+
# end
|
71
|
+
|
72
|
+
# return false
|
73
|
+
# end
|
74
|
+
|
75
|
+
if !certificates.include?(File.read(certificate))
|
76
|
+
Utils.log("Warning", "Your provisioning profile does not include your certificate. Repairing...")
|
77
|
+
return false
|
78
|
+
end
|
79
|
+
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module MotionProvisioning
|
2
|
+
class ProvisioningProfile
|
3
|
+
|
4
|
+
attr_accessor :type, :platform
|
5
|
+
|
6
|
+
def client
|
7
|
+
MotionProvisioning.client
|
8
|
+
end
|
9
|
+
|
10
|
+
def provisioning_profile(bundle_id, app_name, platform, type)
|
11
|
+
self.type = type
|
12
|
+
self.platform = platform
|
13
|
+
provisioning_profile_path = File.expand_path("./provisioning/#{bundle_id}_#{platform}_#{type}_provisioning_profile.mobileprovision")
|
14
|
+
provisioning_profile_name = "(MotionProvisioning) #{bundle_id} #{platform} #{type}"
|
15
|
+
certificate_type = type == :development ? :development : :distribution
|
16
|
+
certificate_platform = platform == :mac ? :mac : :ios
|
17
|
+
certificate_path = File.expand_path("./provisioning/#{certificate_platform}_#{certificate_type}_certificate.cer")
|
18
|
+
if !File.exist?(certificate_path)
|
19
|
+
Utils.log('Error', "Couldn't find the certificate in path '#{certificate_path}'.")
|
20
|
+
Utils.log('Error', "Make sure you're configuring the certificate *before* the provisioning profile in the Rakefile.")
|
21
|
+
abort
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create the folder to store the certs
|
25
|
+
FileUtils.mkdir_p(File.expand_path('./provisioning'))
|
26
|
+
|
27
|
+
if File.exist?(provisioning_profile_path) && ENV['recreate_profile'].nil?
|
28
|
+
mobileprovision = MobileProvision.new(provisioning_profile_path)
|
29
|
+
if mobileprovision.valid?(certificate_path, MotionProvisioning.entitlements)
|
30
|
+
Utils.log('Info', "Using provisioning profile '#{mobileprovision.name}'.")
|
31
|
+
return provisioning_profile_path
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# ensure a client is created and logged in
|
36
|
+
client
|
37
|
+
|
38
|
+
app = Application.find_or_create(bundle_id: bundle_id, name: app_name, mac: platform == :mac)
|
39
|
+
|
40
|
+
profile = profile_type.find_by_bundle_id(bundle_id, mac: platform == :mac).detect do |profile|
|
41
|
+
next if profile.platform.downcase.include?("tvos") && platform != :tvos
|
42
|
+
next if !profile.platform.downcase.include?("tvos") && platform == :tvos
|
43
|
+
profile.name == provisioning_profile_name
|
44
|
+
end
|
45
|
+
|
46
|
+
# Offer to register devices connected to the current computer
|
47
|
+
force_repair = false
|
48
|
+
if [:development, :adhoc].include?(type) && [:ios, :tvos].include?(platform)
|
49
|
+
ids = `/Library/RubyMotion/bin/ios/deploy -D`.split("\n")
|
50
|
+
|
51
|
+
# If there is a profile, we check the device is included.
|
52
|
+
# Otherwise check if the device is registered in the Developer Portal.
|
53
|
+
if profile
|
54
|
+
profile_devices = profile.devices.map(&:udid)
|
55
|
+
ids.each do |id|
|
56
|
+
next if profile_devices.include?(id.downcase)
|
57
|
+
answer = Utils.ask("Info", "This computer is connected to an iOS device with ID '#{id}' which is not included in the profile. Do you want to register it? (Y/n):")
|
58
|
+
if answer.downcase == 'y'
|
59
|
+
Utils.log('Info', "Registering device with ID '#{id}'")
|
60
|
+
Spaceship::Portal::Device.create!(name: 'iOS Device', udid: id)
|
61
|
+
force_repair = true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
else
|
65
|
+
ids.each do |id|
|
66
|
+
existing = Spaceship::Portal::Device.find_by_udid(id)
|
67
|
+
next if existing
|
68
|
+
answer = Utils.ask("Info", "This computer is connected to an iOS device with ID '#{id}' which is not registered in the Developer Portal. Do you want to register it? (Y/n):")
|
69
|
+
if answer.downcase == 'y'
|
70
|
+
Utils.log('Info', "Registering device with ID '#{id}'")
|
71
|
+
client.create_device!('iOS Device', id)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
certificates = if type == :development
|
78
|
+
client.development_certificates(mac: platform == :mac).map { |c| Spaceship::Portal::Certificate.factory(c) }
|
79
|
+
else
|
80
|
+
certificate_platform = platform == :mac ? :mac : :ios
|
81
|
+
certificate_sha1 = OpenSSL::Digest::SHA1.new(File.read(File.expand_path("./provisioning/#{certificate_platform}_distribution_certificate.cer")))
|
82
|
+
cert = client.distribution_certificates(mac: platform == :mac).detect do |c|
|
83
|
+
OpenSSL::Digest::SHA1.new(c['certContent'].read) == certificate_sha1
|
84
|
+
end
|
85
|
+
|
86
|
+
if cert.nil?
|
87
|
+
Utils.log('Error', 'Your distribution certificate is invalid. Recreate it by setting the env variable "recreate_certificate=1" and running the command again.')
|
88
|
+
abort
|
89
|
+
end
|
90
|
+
|
91
|
+
# Distribution profiles can only contain one certificate
|
92
|
+
[Spaceship::Portal::Certificate.factory(cert)]
|
93
|
+
end
|
94
|
+
|
95
|
+
if profile.nil?
|
96
|
+
sub_platform = platform == :tvos ? 'tvOS' : nil
|
97
|
+
Utils.log('Info', 'Could not find any existing profiles, creating a new one.')
|
98
|
+
|
99
|
+
begin
|
100
|
+
profile = profile_type.create!(name: provisioning_profile_name, bundle_id: bundle_id,
|
101
|
+
certificate: certificates , devices: nil, mac: platform == :mac, sub_platform: sub_platform)
|
102
|
+
rescue => ex
|
103
|
+
if ex.to_s.include?("Your team has no devices")
|
104
|
+
Utils.log("Error", "Your team has no devices for which to generate a provisioning profile. Connect a device to use for development or manually add device IDs by running: rake \"motion-provisioning:add-device[device_name,device_id]\"")
|
105
|
+
abort
|
106
|
+
end
|
107
|
+
raise ex
|
108
|
+
end
|
109
|
+
elsif profile.status != 'Active' || profile.certificates.map(&:id) != certificates.map(&:id) || force_repair
|
110
|
+
Utils.log('Info', "Repairing provisioning profile '#{profile.name}'.")
|
111
|
+
profile.certificates = certificates
|
112
|
+
devices = case platform
|
113
|
+
when :tvos then Spaceship::Device.all_apple_tvs
|
114
|
+
when :mac then Spaceship::Device.all_macs
|
115
|
+
else Spaceship::Device.all_ios_profile_devices
|
116
|
+
end
|
117
|
+
profile.devices = type == :distribution ? [] : devices
|
118
|
+
profile = profile.repair!
|
119
|
+
end
|
120
|
+
|
121
|
+
Utils.log('Info', "Using provisioning profile '#{profile.name}'.")
|
122
|
+
File.write(provisioning_profile_path, profile.download)
|
123
|
+
provisioning_profile_path
|
124
|
+
end
|
125
|
+
|
126
|
+
# The kind of provisioning profile we're interested in
|
127
|
+
def profile_type
|
128
|
+
return @profile_type if @profile_type
|
129
|
+
@profile_type = Spaceship.provisioning_profile.app_store
|
130
|
+
@profile_type = Spaceship.provisioning_profile.in_house if client.in_house?
|
131
|
+
@profile_type = Spaceship.provisioning_profile.ad_hoc if self.type == :adhoc
|
132
|
+
@profile_type = Spaceship.provisioning_profile.development if self.type == :development
|
133
|
+
@profile_type
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Spaceship
|
2
|
+
class FreePortalClient < Spaceship::PortalClient
|
3
|
+
|
4
|
+
def create_provisioning_profile!(name, distribution_method, app_id, certificate_ids, device_ids, mac: false, sub_platform: nil)
|
5
|
+
ensure_csrf
|
6
|
+
|
7
|
+
params = {
|
8
|
+
teamId: team_id,
|
9
|
+
appIdId: app_id,
|
10
|
+
}
|
11
|
+
|
12
|
+
r = request_plist(:post, "https://developerservices2.apple.com/services/QH65B2/#{platform_slug(mac)}/downloadTeamProvisioningProfile.action", params)
|
13
|
+
parse_response(r, 'provisioningProfile')
|
14
|
+
end
|
15
|
+
|
16
|
+
def download_provisioning_profile(profile_id, mac: false)
|
17
|
+
r = request_plist(:post, "https://developerservices2.apple.com/services/QH65B2/#{platform_slug(mac)}/downloadProvisioningProfile.action", {
|
18
|
+
teamId: team_id,
|
19
|
+
provisioningProfileId: profile_id
|
20
|
+
})
|
21
|
+
a = parse_response(r, 'provisioningProfile')
|
22
|
+
if a['encodedProfile']
|
23
|
+
a['encodedProfile'].read
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def devices(mac: false)
|
28
|
+
paging do |page_number|
|
29
|
+
r = request_plist(:post, "https://developerservices2.apple.com/services/QH65B2/#{platform_slug(mac)}/listDevices.action", {
|
30
|
+
teamId: team_id,
|
31
|
+
pageNumber: page_number,
|
32
|
+
pageSize: page_size,
|
33
|
+
sort: 'name=asc'
|
34
|
+
})
|
35
|
+
parse_response(r, 'devices')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_device!(device_name, device_id, mac: false)
|
40
|
+
req = request_plist(:post, "https://developerservices2.apple.com/services/#{PROTOCOL_VERSION}/#{platform_slug(mac)}/addDevice.action", {
|
41
|
+
teamId: team_id,
|
42
|
+
deviceNumber: device_id,
|
43
|
+
name: device_name
|
44
|
+
})
|
45
|
+
|
46
|
+
parse_response(req, 'device')
|
47
|
+
end
|
48
|
+
|
49
|
+
def apps(mac: false)
|
50
|
+
paging do |page_number|
|
51
|
+
|
52
|
+
r = request_plist(:post, "https://developerservices2.apple.com/services/QH65B2/#{platform_slug(mac)}/listAppIds.action?clientId=XABBG36SBA", {
|
53
|
+
teamId: team_id,
|
54
|
+
pageNumber: page_number,
|
55
|
+
pageSize: page_size,
|
56
|
+
sort: 'name=asc'
|
57
|
+
})
|
58
|
+
parse_response(r, 'appIds')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def details_for_app(app)
|
63
|
+
r = request_plist(:post, "https://developerservices2.apple.com/services/QH65B2/#{platform_slug(app.mac?)}/getAppIdDetail.action", {
|
64
|
+
teamId: team_id,
|
65
|
+
identifier: app.app_id
|
66
|
+
})
|
67
|
+
parse_response(r, 'appId')
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_app!(type, name, bundle_id, mac: false)
|
71
|
+
params = {
|
72
|
+
identifier: bundle_id,
|
73
|
+
name: name,
|
74
|
+
teamId: team_id
|
75
|
+
}
|
76
|
+
|
77
|
+
ensure_csrf
|
78
|
+
|
79
|
+
r = request_plist(:post, "https://developerservices2.apple.com/services/QH65B2/#{platform_slug(mac)}/addAppId.action?clientId=XABBG36SBA", params)
|
80
|
+
parse_response(r, 'appId')
|
81
|
+
end
|
82
|
+
|
83
|
+
def revoke_development_certificate(serial_number, mac: false)
|
84
|
+
r = request_plist(:post, "https://developerservices2.apple.com/services/QH65B2/#{platform_slug(mac)}/revokeDevelopmentCert.action?clientId=XABBG36SBA", {
|
85
|
+
teamId: team_id,
|
86
|
+
serialNumber: serial_number,
|
87
|
+
})
|
88
|
+
parse_response(r, 'certRequests')
|
89
|
+
end
|
90
|
+
|
91
|
+
def create_development_certificate(csr, mac: false)
|
92
|
+
ensure_csrf
|
93
|
+
|
94
|
+
r = request_plist(:post, "https://developerservices2.apple.com/services/QH65B2/#{platform_slug(mac)}/submitDevelopmentCSR.action?clientId=XABBG36SBA&teamId=#{team_id}", {
|
95
|
+
teamId: team_id,
|
96
|
+
csrContent: csr
|
97
|
+
})
|
98
|
+
|
99
|
+
parse_response(r, 'certRequest')
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def ensure_csrf
|
105
|
+
if csrf_tokens.count == 0
|
106
|
+
# If we directly create a new resource (e.g. app) without querying anything before
|
107
|
+
# we don't have a valid csrf token, that's why we have to do at least one request
|
108
|
+
teams
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def request_plist(method, url_or_path = nil, params = nil, headers = {}, &block)
|
113
|
+
headers['X-Xcode-Version'] = '7.3.1 (7D1014)'
|
114
|
+
headers['Content-Type'] = 'text/x-xml-plist'
|
115
|
+
params = params.to_plist if params
|
116
|
+
headers.merge!(csrf_tokens)
|
117
|
+
headers['User-Agent'] = USER_AGENT
|
118
|
+
response = send_request(method, url_or_path, params, headers, &block)
|
119
|
+
return response
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Spaceship
|
2
|
+
class PortalClient < Spaceship::Client
|
3
|
+
|
4
|
+
def distribution_certificates(mac: false)
|
5
|
+
paging do |page_number|
|
6
|
+
r = request(:post, "https://developerservices2.apple.com/services/QH65B2/#{platform_slug(mac)}/downloadDistributionCerts.action?clientId=XABBG36SBA&teamId=#{team_id}")
|
7
|
+
parse_response(r, 'certificates')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def development_certificates(mac: false)
|
12
|
+
paging do |page_number|
|
13
|
+
r = request(:post, "https://developerservices2.apple.com/services/QH65B2/#{platform_slug(mac)}/listAllDevelopmentCerts.action?clientId=XABBG36SBA&teamId=#{team_id}")
|
14
|
+
parse_response(r, 'certificates')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Fix a bug in Fastlane where the slug is hardcoded to ios
|
19
|
+
def create_certificate!(type, csr, app_id = nil)
|
20
|
+
ensure_csrf
|
21
|
+
|
22
|
+
mac = Spaceship::Portal::Certificate::MAC_CERTIFICATE_TYPE_IDS.keys.include?(type)
|
23
|
+
|
24
|
+
r = request(:post, "account/#{platform_slug(mac)}/certificate/submitCertificateRequest.action", {
|
25
|
+
teamId: team_id,
|
26
|
+
type: type,
|
27
|
+
csrContent: csr,
|
28
|
+
appIdId: app_id # optional
|
29
|
+
})
|
30
|
+
parse_response(r, 'certRequest')
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rake'
|
2
|
+
namespace 'motion-provisioning' do
|
3
|
+
desc 'Add a device to the provisioning portal: rake "motion-provisioning:add-device[device_name,device_id]"'
|
4
|
+
task 'add-device', [:name, :id] do |t, args|
|
5
|
+
name = args[:name]
|
6
|
+
id = args[:id]
|
7
|
+
if name.nil? || id.nil?
|
8
|
+
puts "Missing device name or id."
|
9
|
+
puts "Syntax: rake \"motion-provisioning:add-device[device_name,device_id]\""
|
10
|
+
exit
|
11
|
+
end
|
12
|
+
MotionProvisioning.client.create_device!(name, id)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module MotionProvisioning
|
2
|
+
module Utils
|
3
|
+
module_function
|
4
|
+
def log(what, msg)
|
5
|
+
require 'thread'
|
6
|
+
@print_mutex ||= Mutex.new
|
7
|
+
# Because this method can be called concurrently, we don't want to mess any output.
|
8
|
+
@print_mutex.synchronize do
|
9
|
+
$stderr.puts(what(what) + ' ' + msg)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def ask(what, question)
|
14
|
+
what = "\e[1m" + what.rjust(10) + "\e[0m" # bold
|
15
|
+
$stderr.print(what(what) + ' ' + question + ' ')
|
16
|
+
$stderr.flush
|
17
|
+
|
18
|
+
result = $stdin.gets
|
19
|
+
result.chomp! if result
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def ask_password(what, question)
|
24
|
+
require 'io/console' # needed for noecho
|
25
|
+
|
26
|
+
# Save current buffering mode
|
27
|
+
buffering = $stderr.sync
|
28
|
+
|
29
|
+
# Turn off buffering
|
30
|
+
$stderr.sync = true
|
31
|
+
`stty -icanon`
|
32
|
+
|
33
|
+
begin
|
34
|
+
$stderr.print(what(what) + ' ' + question + ' ')
|
35
|
+
$stderr.flush
|
36
|
+
pw = ""
|
37
|
+
|
38
|
+
$stderr.noecho do
|
39
|
+
while ( char = $stdin.getc ) != "\n" # break after [Enter]
|
40
|
+
putc "*"
|
41
|
+
pw << char
|
42
|
+
end
|
43
|
+
end
|
44
|
+
ensure
|
45
|
+
print "\n"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Restore original buffering mode
|
49
|
+
$stderr.sync = buffering
|
50
|
+
|
51
|
+
`stty -icanon`
|
52
|
+
pw
|
53
|
+
end
|
54
|
+
|
55
|
+
def what(what)
|
56
|
+
"\e[1m" + what.rjust(10) + "\e[0m" # bold
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|