motion-provisioning 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|