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.
@@ -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