motion-provisioning 0.0.1

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