spaceship 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,72 @@
1
- require "spaceship/version"
2
- require "spaceship/ship"
1
+ require 'spaceship/version'
2
+ require 'spaceship/base'
3
+ require 'spaceship/client'
4
+ require 'spaceship/profile_types'
5
+ require 'spaceship/app'
6
+ require 'spaceship/certificate'
7
+ require 'spaceship/device'
8
+ require 'spaceship/provisioning_profile'
9
+ require 'spaceship/launcher'
3
10
 
4
11
  module Spaceship
5
- # Your code goes here...
12
+ # Use this to just setup the configuration attribute and set it later somewhere else
13
+ class << self
14
+ # This client stores the default client when using the lazy syntax
15
+ # Spaceship.app instead of using the spaceship launcher
16
+ attr_accessor :client
17
+
18
+ # Authenticates with Apple's web services. This method has to be called once
19
+ # to generate a valid session. The session will automatically be used from then
20
+ # on.
21
+ #
22
+ # This method will automatically use the username from the Appfile (if available)
23
+ # and fetch the password from the Keychain (if available)
24
+ #
25
+ # @param user (String) (optional): The username (usually the email address)
26
+ # @param password (String) (optional): The password
27
+ #
28
+ # @raise InvalidUserCredentialsError: raised if authentication failed
29
+ #
30
+ # @return (Spaceship::Client) The client the login method was called for
31
+ def login(user = nil, password = nil)
32
+ @client = Client.login(user, password)
33
+ end
34
+
35
+ # Open up the team selection for the user (if necessary).
36
+ #
37
+ # If the user is in multiple teams, a team selection is shown.
38
+ # The user can then select a team by entering the number
39
+ #
40
+ # Additionally, the team ID is shown next to each team name
41
+ # so that the user can use the environment variable `FASTLANE_TEAM_ID`
42
+ # for future user.
43
+ #
44
+ # @return (String) The ID of the select team. You also get the value if
45
+ # the user is only in one team.
46
+ def select_team
47
+ @client.select_team
48
+ end
49
+
50
+ # Helper methods for managing multiple instances of spaceship
51
+
52
+ # @return (Class) Access the apps for the spaceship
53
+ def app
54
+ Spaceship::App.set_client(@client)
55
+ end
56
+
57
+ # @return (Class) Access the devices for the spaceship
58
+ def device
59
+ Spaceship::Device.set_client(@client)
60
+ end
61
+
62
+ # @return (Class) Access the certificates for the spaceship
63
+ def certificate
64
+ Spaceship::Certificate.set_client(@client)
65
+ end
66
+
67
+ # @return (Class) Access the provisioning profiles for the spaceship
68
+ def provisioning_profile
69
+ Spaceship::ProvisioningProfile.set_client(@client)
70
+ end
71
+ end
6
72
  end
@@ -0,0 +1,96 @@
1
+ module Spaceship
2
+ # Represents an App ID from the Developer Portal
3
+ class App < Base
4
+
5
+ # @return (String) The identifier of this app, provided by the Dev Portal
6
+ # @example
7
+ # "RGAWZGXSAA"
8
+ attr_accessor :app_id
9
+
10
+ # @return (String) The name you provided for this app
11
+ # @example
12
+ # "Spaceship"
13
+ attr_accessor :name
14
+
15
+ # @return (String) the supported platform of this app
16
+ # @example
17
+ # "ios"
18
+ attr_accessor :platform
19
+
20
+ # Prefix provided by the Dev Portal
21
+ # @example
22
+ # "5A997XSHK2"
23
+ attr_accessor :prefix
24
+
25
+ # @return (String) The bundle_id (app identifier) of your app
26
+ # @example
27
+ # "com.krausefx.app"
28
+ attr_accessor :bundle_id
29
+
30
+ # @return (Bool) Is this app a wildcard app (e.g. com.krausefx.*)
31
+ attr_accessor :is_wildcard
32
+
33
+ # @return (Bool) Development Push Enabled?
34
+ attr_accessor :dev_push_enabled
35
+
36
+ # @return (Bool) Production Push Enabled?
37
+ attr_accessor :prod_push_enabled
38
+
39
+ attr_mapping(
40
+ 'appIdId' => :app_id,
41
+ 'name' => :name,
42
+ 'appIdPlatform' => :platform,
43
+ 'prefix' => :prefix,
44
+ 'identifier' => :bundle_id,
45
+ 'isWildCard' => :is_wildcard,
46
+ 'isDevPushEnabled' => :dev_push_enabled,
47
+ 'isProdPushEnabled' => :prod_push_enabled
48
+ )
49
+
50
+ class << self
51
+ # Create a new object based on a hash.
52
+ # This is used to create a new object based on the server response.
53
+ def factory(attrs)
54
+ self.new(attrs)
55
+ end
56
+
57
+ # @return (Array) Returns all apps available for this account
58
+ def all
59
+ client.apps.map { |app| self.factory(app) }
60
+ end
61
+
62
+ # Creates a new App ID on the Apple Dev Portal
63
+ #
64
+ # if bundle_id ends with '*' then it is a wildcard id otherwise, it is an explicit id
65
+ # @param bundle_id [String] the bundle id (app_identifier) of the app associated with this provisioning profile
66
+ # @param name [String] the name of the App
67
+ # @return (App) The app you just created
68
+ def create!(bundle_id: bundle_id, name: name)
69
+ if bundle_id.end_with?('*')
70
+ type = :wildcard
71
+ else
72
+ type = :explicit
73
+ end
74
+
75
+ new_app = client.create_app!(type, name, bundle_id)
76
+ self.new(new_app)
77
+ end
78
+
79
+ # Find a specific App ID based on the bundle_id
80
+ # @return (App) The app you're looking for. This is nil if the app can't be found.
81
+ def find(bundle_id)
82
+ all.find do |app|
83
+ app.bundle_id == bundle_id
84
+ end
85
+ end
86
+ end
87
+
88
+ # Delete this App ID. This action will most likely fail if the App ID is already in the store
89
+ # or there are active profiles
90
+ # @return (App) The app you just deletd
91
+ def delete!
92
+ client.delete_app!(app_id)
93
+ self
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,86 @@
1
+ module Spaceship
2
+ class Base
3
+ class << self
4
+ attr_accessor :client
5
+
6
+ def client
7
+ @client || Spaceship.client
8
+ end
9
+
10
+ #set client and return self for chaining
11
+ def set_client(client)
12
+ self.client = client
13
+ self
14
+ end
15
+
16
+ ##
17
+ # bang method since it changes the parameter in-place
18
+ def remap_keys!(attrs)
19
+ return if attr_mapping.nil?
20
+
21
+ attr_mapping.each do |from, to|
22
+ attrs[to] = attrs.delete(from)
23
+ end
24
+ end
25
+
26
+ def attr_mapping(attr_map = nil)
27
+ if attr_map
28
+ @attr_mapping = attr_map
29
+ else
30
+ @attr_mapping ||= ancestors[1].attr_mapping rescue nil
31
+ end
32
+ end
33
+
34
+ ##
35
+ # Call a method to return a subclass constant.
36
+ #
37
+ # If `method_sym` is an underscored name of a class,
38
+ # return the class with the current client passed into it.
39
+ # If the method does not match, NoMethodError is raised.
40
+ #
41
+ # Example:
42
+ #
43
+ # Certificate.production_push
44
+ # #=> Certificate::ProductionPush
45
+ #
46
+ # ProvisioningProfile.ad_hoc
47
+ # #=> ProvisioningProfile::AdHoc
48
+ #
49
+ # ProvisioningProfile.some_other_method
50
+ # #=> NoMethodError: undefined method `some_other_method' for ProvisioningProfile
51
+ def method_missing(method_sym, *args, &block)
52
+ module_name = method_sym.to_s
53
+ module_name.sub!(/^[a-z\d]/) { $&.upcase }
54
+ module_name.gsub!(/(?:_|(\/))([a-z\d])/) { $2.upcase }
55
+ const_name = "#{self.name}::#{module_name}"
56
+ # if const_defined?(const_name)
57
+ klass = const_get(const_name)
58
+ klass.set_client(@client)
59
+ # else
60
+ # super
61
+ # end
62
+ end
63
+ end
64
+
65
+ def initialize(attrs = {})
66
+ self.class.remap_keys!(attrs)
67
+ attrs.each do |key, val|
68
+ self.send("#{key}=", val) if respond_to?("#{key}=")
69
+ end
70
+ @client = self.class.client
71
+ end
72
+
73
+ def client
74
+ @client
75
+ end
76
+
77
+ def inspect
78
+ inspectables = instance_variables - [:@client]
79
+ inspect_vars = inspectables.map do |ivar|
80
+ val = instance_variable_get(ivar)
81
+ "#{ivar}=#{val.inspect}"
82
+ end
83
+ "\n#<#{self.class.name}\n\t#{inspect_vars.join("\n\t")}>"
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,270 @@
1
+ require 'openssl'
2
+
3
+ module Spaceship
4
+ # Represents a certificate from the Apple Developer Portal.
5
+ #
6
+ # This can either be a code signing identity or a push profile
7
+ class Certificate < Base
8
+ # @return (String) The ID given from the developer portal. You'll probably not need it.
9
+ # @example
10
+ # "P577TH3PAA"
11
+ attr_accessor :id
12
+
13
+ # @return (String) The name of the certificate
14
+ # @example Company
15
+ # "SunApps GmbH"
16
+ # @example Push Profile
17
+ # "com.krausefx.app"
18
+ attr_accessor :name
19
+
20
+ # @return (String) Status of the certificate
21
+ # @example
22
+ # "Issued"
23
+ attr_accessor :status
24
+
25
+ # @return (Date) The date and time when the certificate was created
26
+ # @example
27
+ # 2015-04-01 21:24:00 UTC
28
+ attr_accessor :created
29
+
30
+ # @return (Date) The date and time when the certificate will expire
31
+ # @example
32
+ # 2016-04-01 21:24:00 UTC
33
+ attr_accessor :expires
34
+
35
+ # @return (String) The owner type that defines if it's a push profile
36
+ # or a code signing identity
37
+ #
38
+ # @example Code Signing Identity
39
+ # "team"
40
+ # @example Push Certificate
41
+ # "bundle"
42
+ attr_accessor :owner_type
43
+
44
+ # @return (String) The name of the owner
45
+ #
46
+ # @example Code Signing Identity (usually the company name)
47
+ # "SunApps Gmbh"
48
+ # @example Push Certificate (the name of your App ID)
49
+ # "Awesome App"
50
+ attr_accessor :owner_name
51
+
52
+ # @return (String) The ID of the owner, that can be used to
53
+ # fetch more information
54
+ # @example
55
+ # "75B83SPLAA"
56
+ attr_accessor :owner_id
57
+
58
+ # Indicates the type of this certificate
59
+ # which is automatically used to determine the class of
60
+ # the certificate. Available values listed in CERTIFICATE_TYPE_IDS
61
+ # @return (String) The type of the certificate
62
+ # @example Production Certificate
63
+ # "R58UK2EWSO"
64
+ # @example Development Certificate
65
+ # "5QPB9NHCEI"
66
+ attr_accessor :type_display_id
67
+
68
+ attr_mapping({
69
+ 'certificateId' => :id,
70
+ 'name' => :name,
71
+ 'statusString' => :status,
72
+ 'dateCreated' => :created,
73
+ 'expirationDate' => :expires,
74
+ 'ownerType' => :owner_type,
75
+ 'ownerName' => :owner_name,
76
+ 'ownerId' => :owner_id,
77
+ 'certificateTypeDisplayId' => :type_display_id
78
+ })
79
+
80
+ #####################################################
81
+ # Certs are not associated with apps
82
+ #####################################################
83
+
84
+ # A development code signing certificate used for development environment
85
+ class Development < Certificate; end
86
+
87
+ # A production code signing certificate used for distribution environment
88
+ class Production < Certificate; end
89
+
90
+ # An In House code signing certificate used for enterprise distributions
91
+ class InHouse < Certificate; end
92
+
93
+ #####################################################
94
+ # Certs that are specific for one app
95
+ #####################################################
96
+
97
+ # Abstract class for push certificates. Check out the subclasses
98
+ # DevelopmentPush, ProductionPush, WebsitePush and VoipPush
99
+ class PushCertificate < Certificate; end
100
+
101
+ # A push notification certificate for development environment
102
+ class DevelopmentPush < PushCertificate; end
103
+
104
+ # A push notification certificate for production environment
105
+ class ProductionPush < PushCertificate; end
106
+
107
+ # A push notification certificate for websites
108
+ class WebsitePush < PushCertificate; end
109
+
110
+ # A push notification certificate for the VOIP environment
111
+ class VoipPush < PushCertificate; end
112
+
113
+ # Passbook certificate
114
+ class Passbook < Certificate; end
115
+
116
+ # ApplePay certificate
117
+ class ApplePay < Certificate; end
118
+
119
+ CERTIFICATE_TYPE_IDS = {
120
+ "5QPB9NHCEI" => Development,
121
+ "R58UK2EWSO" => Production,
122
+ "9RQEK7MSXA" => InHouse,
123
+ "LA30L5BJEU" => Certificate,
124
+ "BKLRAVXMGM" => DevelopmentPush,
125
+ "3BQKVH9I2X" => ProductionPush,
126
+ "Y3B2F3TYSI" => Passbook,
127
+ "3T2ZP62QW8" => WebsitePush,
128
+ "E5D663CMZW" => WebsitePush,
129
+ "4APLUP237T" => ApplePay
130
+ }
131
+
132
+ #class methods
133
+ class << self
134
+ # Create a new code signing request that can be used to
135
+ # generate a new certificate
136
+ # @example
137
+ # Create a new certificate signing request
138
+ # csr, pkey = Spaceship.certificate.create_certificate_signing_request
139
+ #
140
+ # # Use the signing request to create a new distribution certificate
141
+ # Spaceship.certificate.production.create!(csr: csr)
142
+ def create_certificate_signing_request
143
+ key = OpenSSL::PKey::RSA.new 2048
144
+ csr = OpenSSL::X509::Request.new
145
+ csr.version = 0
146
+ csr.subject = OpenSSL::X509::Name.new([
147
+ ['CN', 'PEM', OpenSSL::ASN1::UTF8STRING]
148
+ ])
149
+ csr.public_key = key.public_key
150
+ csr.sign(key, OpenSSL::Digest::SHA1.new)
151
+ return [csr, key]
152
+ end
153
+
154
+ # Create a new object based on a hash.
155
+ # This is used to create a new object based on the server response.
156
+ def factory(attrs)
157
+ # Example:
158
+ # => {"name"=>"iOS Distribution: SunApps GmbH",
159
+ # "certificateId"=>"XC5PH8DAAA",
160
+ # "serialNumber"=>"797E732CCE8B7AAA",
161
+ # "status"=>"Issued",
162
+ # "statusCode"=>0,
163
+ # "expirationDate"=>#<DateTime: 2015-11-25T22:45:50+00:00 ((2457352j,81950s,0n),+0s,2299161j)>,
164
+ # "certificatePlatform"=>"ios",
165
+ # "certificateType"=>
166
+ # {"certificateTypeDisplayId"=>"R58UK2EAAA",
167
+ # "name"=>"iOS Distribution",
168
+ # "platform"=>"ios",
169
+ # "permissionType"=>"distribution",
170
+ # "distributionType"=>"store",
171
+ # "distributionMethod"=>"app",
172
+ # "ownerType"=>"team",
173
+ # "daysOverlap"=>364,
174
+ # "maxActive"=>2}}
175
+
176
+ if attrs['certificateType']
177
+ # On some accounts this is nested, so we need to flatten it
178
+ attrs.merge!(attrs['certificateType'])
179
+ attrs.delete('certificateType')
180
+ end
181
+
182
+ # Parse the dates
183
+ attrs['expirationDate'] = (Time.parse(attrs['expirationDate']) rescue attrs['expirationDate'])
184
+ attrs['dateCreated'] = (Time.parse(attrs['dateCreated']) rescue attrs['dateCreated'])
185
+
186
+ # Here we go
187
+ klass = CERTIFICATE_TYPE_IDS[attrs['certificateTypeDisplayId']]
188
+ klass ||= Certificate
189
+ klass.client = @client
190
+ klass.new(attrs)
191
+ end
192
+
193
+ # @return (Array) Returns all certificates of this account.
194
+ # If this is called from a subclass of Certificate, this will
195
+ # only include certificates matching the current type.
196
+ def all
197
+ if (self == Certificate) # are we the base-class?
198
+ types = CERTIFICATE_TYPE_IDS.keys
199
+ else
200
+ types = [CERTIFICATE_TYPE_IDS.key(self)]
201
+ end
202
+
203
+ client.certificates(types).map do |cert|
204
+ factory(cert)
205
+ end
206
+ end
207
+
208
+ # @return (Certificate) Find a certificate based on the ID of the certificate.
209
+ def find(certificate_id)
210
+ all.find do |c|
211
+ c.id == certificate_id
212
+ end
213
+ end
214
+
215
+ # Generate a new certificate based on a code certificate signing request
216
+ # @param csr (required): The certificate signing request to use. Get one using
217
+ # `create_certificate_signing_request`
218
+ # @param bundle_id (String) (optional): The app identifier this certificate is for.
219
+ # This value is only needed if you create a push profile. For normal code signing
220
+ # certificates, you must only pass a certificate signing request.
221
+ # @example
222
+ # # Create a new certificate signing request
223
+ # csr, pkey = Spaceship::Certificate.create_certificate_signing_request
224
+ #
225
+ # # Use the signing request to create a new distribution certificate
226
+ # Spaceship::Certificate::Production.create!(csr: csr)
227
+ # @return (Device): The newly created device
228
+ def create!(csr: csr, bundle_id: nil)
229
+ type = CERTIFICATE_TYPE_IDS.key(self)
230
+
231
+ # look up the app_id by the bundle_id
232
+ if bundle_id
233
+ app = Spaceship::App.find(bundle_id)
234
+ raise "Could not find app with bundle id '#{bundle_id}'" unless app
235
+ app_id = app.app_id
236
+ end
237
+
238
+ # if this succeeds, we need to save the .cer and the private key in keychain access or wherever they go in linux
239
+ response = client.create_certificate!(type, csr.to_pem, app_id)
240
+ # munge the response to make it work for the factory
241
+ response['certificateTypeDisplayId'] = response['certificateType']['certificateTypeDisplayId']
242
+ self.new(response)
243
+ end
244
+ end
245
+
246
+ # instance methods
247
+
248
+ # @return (String) Download the raw data of the certificate without parsing
249
+ def download_raw
250
+ client.download_certificate(id, type_display_id)
251
+ end
252
+
253
+ # @return (OpenSSL::X509::Certificate) Downloads and parses the certificate
254
+ def download
255
+ OpenSSL::X509::Certificate.new(download_raw)
256
+ end
257
+
258
+ # Revoke the certificate. You shouldn't use this method probably.
259
+ def revoke!
260
+ client.revoke_certificate!(id, type_display_id)
261
+ end
262
+
263
+ # @return (Bool): Is this certificate a push profile for apps?
264
+ def is_push?
265
+ # does display_type_id match push?
266
+ [Client::ProfileTypes::Push.development, Client::ProfileTypes::Push.production].include?(type_display_id)
267
+ end
268
+
269
+ end
270
+ end