fastlane 2.115.0.beta.20190119200019 → 2.115.0.beta.20190120200101

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 405a7ac48ad02e28a6b26a3dda7458028411060b
4
- data.tar.gz: c1df6b742f5a232012d94e42d2e04c935a3ec201
3
+ metadata.gz: ca7d2255218f2b7f48d1b95b3d48abee6957abd2
4
+ data.tar.gz: a61e4ceef9c859240a85d52a4046c8aab11ad10b
5
5
  SHA512:
6
- metadata.gz: e072c405cda86946bac1343f0a8d9de9ac72ee822385443633ed550022f6fdfd331d78d9d50c632fb7afdc30d8d5d348d837e8df632ca5645b79ce1769e87ced
7
- data.tar.gz: 8a939edd7c093b7c4a291ac5651fcaafa77910279fdf13bb65a1c6d629458bd5f98b42701bb2ffc34698f7d201a3990043ace5cf8c25865e95b9264f4d54adaf
6
+ metadata.gz: 3e69aacb4427d3d84b4f348ecf78cb0eb83e3944e770f9d17673e25765b0ae4cfebb71ef258efd9cee63f79057fb0a88bb1de1aadc5efd5e3470606f03791226
7
+ data.tar.gz: 3a43f626ddbf582c1b5666c544a103d2ffead5b9879db53194de284033323d75a08f082ae51afc264968754ceb282a146a82238610a995caad73d92ceb739a88
@@ -254,7 +254,9 @@ module Fastlane
254
254
  branch_option = "--branch #{branch}" if branch != 'HEAD'
255
255
 
256
256
  UI.message("Cloning remote git repo...")
257
- Actions.sh("GIT_TERMINAL_PROMPT=0 git clone '#{url}' '#{clone_folder}' --depth 1 -n #{branch_option}")
257
+ Helper.with_env_values('GIT_TERMINAL_PROMPT' => '0') do
258
+ Actions.sh("git clone #{url.shellescape} #{clone_folder.shellescape} --depth 1 -n #{branch_option}")
259
+ end
258
260
 
259
261
  unless version.nil?
260
262
  req = Gem::Requirement.new(version)
@@ -263,14 +265,14 @@ module Fastlane
263
265
  UI.user_error!("No tag found matching #{version.inspect}") if checkout_param.nil?
264
266
  end
265
267
 
266
- Actions.sh("cd '#{clone_folder}' && git checkout #{checkout_param} '#{path}'")
268
+ Actions.sh("cd #{clone_folder.shellescape} && git checkout #{checkout_param.shellescape} #{path.shellescape}")
267
269
 
268
270
  # We also want to check out all the local actions of this fastlane setup
269
271
  containing = path.split(File::SEPARATOR)[0..-2]
270
272
  containing = "." if containing.count == 0
271
273
  actions_folder = File.join(containing, "actions")
272
274
  begin
273
- Actions.sh("cd '#{clone_folder}' && git checkout #{checkout_param} '#{actions_folder}'")
275
+ Actions.sh("cd #{clone_folder.shellescape} && git checkout #{checkout_param.shellescape} #{actions_folder.shellescape}")
274
276
  rescue
275
277
  # We don't care about a failure here, as local actions are optional
276
278
  end
@@ -290,10 +292,12 @@ module Fastlane
290
292
 
291
293
  def fetch_remote_tags(folder: nil)
292
294
  UI.message("Fetching remote git tags...")
293
- Actions.sh("cd '#{folder}' && GIT_TERMINAL_PROMPT=0 git fetch --all --tags -q")
295
+ Helper.with_env_values('GIT_TERMINAL_PROMPT' => '0') do
296
+ Actions.sh("cd #{folder.shellescape} && git fetch --all --tags -q")
297
+ end
294
298
 
295
299
  # Fetch all possible tags
296
- git_tags_string = Actions.sh("cd '#{folder}' && git tag -l")
300
+ git_tags_string = Actions.sh("cd #{folder.shellescape} && git tag -l")
297
301
  git_tags = git_tags_string.split("\n")
298
302
 
299
303
  # Sort tags based on their version number
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
- VERSION = '2.115.0.beta.20190119200019'.freeze
2
+ VERSION = '2.115.0.beta.20190120200101'.freeze
3
3
  DESCRIPTION = "The easiest way to automate beta deployments and releases for your iOS and Android apps".freeze
4
4
  MINIMUM_XCODE_RELEASE = "7.0".freeze
5
5
  RUBOCOP_REQUIREMENT = '0.49.1'.freeze
@@ -299,6 +299,22 @@ module FastlaneCore
299
299
  Helper.backticks(command, print: print)
300
300
  end
301
301
 
302
+ # Executes the provided block after adjusting the ENV to have the
303
+ # provided keys and values set as defined in hash. After the block
304
+ # completes, restores the ENV to its previous state.
305
+ def self.with_env_values(hash, &block)
306
+ old_vals = ENV.select { |k, v| hash.include?(k) }
307
+ hash.each do |k, v|
308
+ ENV[k] = hash[k]
309
+ end
310
+ yield
311
+ ensure
312
+ hash.each do |k, v|
313
+ ENV.delete(k) unless old_vals.include?(k)
314
+ ENV[k] = old_vals[k]
315
+ end
316
+ end
317
+
302
318
  # loading indicator
303
319
  #
304
320
 
@@ -91,10 +91,16 @@ module FastlaneCore
91
91
  err = "#{dir}/cms.err"
92
92
  # we want to prevent the error output to mix up with the standard output because of
93
93
  # /dev/null: https://github.com/fastlane/fastlane/issues/6387
94
- if keychain_path.nil?
95
- decoded = `security cms -D -i "#{path}" 2> #{err}`
94
+ if Helper.mac?
95
+ if keychain_path.nil?
96
+ decoded = `security cms -D -i "#{path}" 2> #{err}`
97
+ else
98
+ decoded = `security cms -D -i "#{path}" -k "#{keychain_path.shellescape}" 2> #{err}`
99
+ end
96
100
  else
97
- decoded = `security cms -D -i "#{path}" -k "#{keychain_path.shellescape}" 2> #{err}`
101
+ # `security` only works on Mac, fallback to `openssl`
102
+ # via https://stackoverflow.com/a/14379814/252627
103
+ decoded = `openssl smime -inform der -verify -noverify -in #{path} 2> #{err}`
98
104
  end
99
105
  UI.error("Failure to decode #{path}. Exit: #{$?.exitstatus}: #{File.read(err)}") if $?.exitstatus != 0
100
106
  decoded
@@ -116,7 +116,7 @@ module Match
116
116
  def encrypt_specific_file(path: nil, password: nil)
117
117
  UI.user_error!("No password supplied") if password.to_s.strip.length == 0
118
118
 
119
- data_to_encrypt = File.read(path)
119
+ data_to_encrypt = File.binread(path)
120
120
  salt = SecureRandom.random_bytes(8)
121
121
 
122
122
  # The :: is important, as there is a name clash
@@ -174,6 +174,10 @@ module Match
174
174
  description: "The name of provisioning profile template. If the developer account has provisioning profile templates (aka: custom entitlements), the template name can be found by inspecting the Entitlements drop-down while creating/editing a provisioning profile (e.g. \"Apple Pay Pass Suppression Development\")",
175
175
  optional: true,
176
176
  default_value: nil),
177
+ FastlaneCore::ConfigItem.new(key: :output_path,
178
+ env_name: "MATCH_OUTPUT_PATH",
179
+ description: "Path in which to export certificates, key and profile",
180
+ optional: true),
177
181
 
178
182
  # other
179
183
  FastlaneCore::ConfigItem.new(key: :verbose,
@@ -25,6 +25,8 @@ module Match
25
25
  def run(params)
26
26
  self.files_to_commit = []
27
27
 
28
+ FileUtils.mkdir_p(params[:output_path]) if params[:output_path]
29
+
28
30
  FastlaneCore::PrintTable.print_values(config: params,
29
31
  title: "Summary for match #{Fastlane::VERSION}")
30
32
 
@@ -166,21 +168,31 @@ module Match
166
168
  self.files_to_commit << private_key_path
167
169
  else
168
170
  cert_path = certs.last
169
- UI.message("Installing certificate...")
170
171
 
171
- # Only looking for cert in "custom" (non login.keychain) keychain
172
- # Doing this for backwards compatability
173
- keychain_name = params[:keychain_name] == "login.keychain" ? nil : params[:keychain_name]
172
+ if Helper.mac?
173
+ UI.message("Installing certificate...")
174
+
175
+ # Only looking for cert in "custom" (non login.keychain) keychain
176
+ # Doing this for backwards compatability
177
+ keychain_name = params[:keychain_name] == "login.keychain" ? nil : params[:keychain_name]
178
+
179
+ if FastlaneCore::CertChecker.installed?(cert_path, in_keychain: keychain_name)
180
+ UI.verbose("Certificate '#{File.basename(cert_path)}' is already installed on this machine")
181
+ else
182
+ Utils.import(cert_path, params[:keychain_name], password: params[:keychain_password])
183
+ end
174
184
 
175
- if FastlaneCore::CertChecker.installed?(cert_path, in_keychain: keychain_name)
176
- UI.verbose("Certificate '#{File.basename(cert_path)}' is already installed on this machine")
185
+ # Import the private key
186
+ # there seems to be no good way to check if it's already installed - so just install it
187
+ Utils.import(keys.last, params[:keychain_name], password: params[:keychain_password])
177
188
  else
178
- Utils.import(cert_path, params[:keychain_name], password: params[:keychain_password])
189
+ UI.message("Skipping installation of certificate as it would not work on this operating system.")
179
190
  end
180
191
 
181
- # Import the private key
182
- # there seems to be no good way to check if it's already installed - so just install it
183
- Utils.import(keys.last, params[:keychain_name], password: params[:keychain_password])
192
+ if params[:output_path]
193
+ FileUtils.cp(cert_path, params[:output_path])
194
+ FileUtils.cp(keys.last, params[:output_path])
195
+ end
184
196
 
185
197
  # Get and print info of certificate
186
198
  info = Utils.get_cert_info(cert_path)
@@ -202,7 +214,9 @@ module Match
202
214
  profile_name = names.join("_").gsub("*", '\*') # this is important, as it shouldn't be a wildcard
203
215
  base_dir = File.join(prefixed_working_directory(working_directory), "profiles", prov_type.to_s)
204
216
  profiles = Dir[File.join(base_dir, "#{profile_name}.mobileprovision")]
205
- keychain_path = FastlaneCore::Helper.keychain_path(params[:keychain_name]) unless params[:keychain_name].nil?
217
+ if Helper.mac?
218
+ keychain_path = FastlaneCore::Helper.keychain_path(params[:keychain_name]) unless params[:keychain_name].nil?
219
+ end
206
220
 
207
221
  # Install the provisioning profiles
208
222
  profile = profiles.last
@@ -220,11 +234,13 @@ module Match
220
234
 
221
235
  if profile.nil? || params[:force]
222
236
  if params[:readonly]
223
- all_profiles = Dir.entries(base_dir).reject { |f| f.start_with?(".") }
224
237
  UI.error("No matching provisioning profiles found for '#{profile_name}'")
225
238
  UI.error("A new one cannot be created because you enabled `readonly`")
226
- UI.error("Provisioning profiles in your repo for type `#{prov_type}`:")
227
- all_profiles.each { |p| UI.error("- '#{p}'") }
239
+ if Dir.exist?(base_dir) # folder for `prov_type` does not exist on first match use for that type
240
+ all_profiles = Dir.entries(base_dir).reject { |f| f.start_with?(".") }
241
+ UI.error("Provisioning profiles in your repo for type `#{prov_type}`:")
242
+ all_profiles.each { |p| UI.error("- '#{p}'") }
243
+ end
228
244
  UI.error("If you are certain that a profile should exist, double-check the recent changes to your match repository")
229
245
  UI.user_error!("No matching provisioning profiles found and can not create a new one because you enabled `readonly`. Check the output above for more information.")
230
246
  end
@@ -236,10 +252,16 @@ module Match
236
252
  self.files_to_commit << profile
237
253
  end
238
254
 
239
- installed_profile = FastlaneCore::ProvisioningProfile.install(profile, keychain_path)
255
+ if Helper.mac?
256
+ installed_profile = FastlaneCore::ProvisioningProfile.install(profile, keychain_path)
257
+ end
240
258
  parsed = FastlaneCore::ProvisioningProfile.parse(profile, keychain_path)
241
259
  uuid = parsed["UUID"]
242
260
 
261
+ if params[:output_path]
262
+ FileUtils.cp(profile, params[:output_path])
263
+ end
264
+
243
265
  if spaceship && !spaceship.profile_exists(username: params[:username], uuid: uuid)
244
266
  # This profile is invalid, let's remove the local file and generate a new one
245
267
  File.delete(profile)
@@ -60,7 +60,7 @@ module Match
60
60
  # No existing working directory, creating a new one now
61
61
  self.working_directory = Dir.mktmpdir
62
62
 
63
- command = "git clone '#{self.git_url}' '#{self.working_directory}'"
63
+ command = "git clone #{self.git_url.shellescape} #{self.working_directory.shellescape}"
64
64
  if self.shallow_clone
65
65
  command << " --depth 1 --no-single-branch"
66
66
  elsif self.clone_branch_directly
@@ -74,9 +74,11 @@ module Match
74
74
 
75
75
  begin
76
76
  # GIT_TERMINAL_PROMPT will fail the `git clone` command if user credentials are missing
77
- FastlaneCore::CommandExecutor.execute(command: "GIT_TERMINAL_PROMPT=0 #{command}",
78
- print_all: FastlaneCore::Globals.verbose?,
79
- print_command: FastlaneCore::Globals.verbose?)
77
+ Helper.with_env_values('GIT_TERMINAL_PROMPT' => '0') do
78
+ FastlaneCore::CommandExecutor.execute(command: command,
79
+ print_all: FastlaneCore::Globals.verbose?,
80
+ print_command: FastlaneCore::Globals.verbose?)
81
+ end
80
82
  rescue
81
83
  UI.error("Error cloning certificates repo, please make sure you have read access to the repository you want to use")
82
84
  if self.branch && self.clone_branch_directly
@@ -193,13 +195,15 @@ module Match
193
195
  def git_push(commands: [], commit_message: nil)
194
196
  commit_message ||= generate_commit_message
195
197
  commands << "git commit -m #{commit_message.shellescape}"
196
- commands << "GIT_TERMINAL_PROMPT=0 git push origin #{self.branch.shellescape}"
198
+ commands << "git push origin #{self.branch.shellescape}"
197
199
 
198
200
  UI.message("Pushing changes to remote git repo...")
199
- commands.each do |command|
200
- FastlaneCore::CommandExecutor.execute(command: command,
201
+ Helper.with_env_values('GIT_TERMINAL_PROMPT' => '0') do
202
+ commands.each do |command|
203
+ FastlaneCore::CommandExecutor.execute(command: command,
201
204
  print_all: FastlaneCore::Globals.verbose?,
202
- print_command: FastlaneCore::Globals.verbose?)
205
+ print_command: FastlaneCore::Globals.verbose?)
206
+ end
203
207
  end
204
208
 
205
209
  self.clear_changes
@@ -32,7 +32,7 @@ module Match
32
32
  end
33
33
 
34
34
  def self.get_cert_info(cer_certificate_path)
35
- cert = OpenSSL::X509::Certificate.new(File.read(cer_certificate_path))
35
+ cert = OpenSSL::X509::Certificate.new(File.binread(cer_certificate_path))
36
36
 
37
37
  # openssl output:
38
38
  # subject= /UID={User ID}/CN={Certificate Name}/OU={Certificate User}/O={Organisation}/C={Country}
@@ -54,7 +54,7 @@ module Match
54
54
  .push([openssl_keys_to_readable_keys.fetch("notBefore"), cert.not_before])
55
55
  .push([openssl_keys_to_readable_keys.fetch("notAfter"), cert.not_after])
56
56
  rescue => ex
57
- UI.error(ex)
57
+ UI.error("get_cert_info: #{ex}")
58
58
  return {}
59
59
  end
60
60
 
@@ -3,6 +3,7 @@ require 'credentials_manager/appfile_config'
3
3
  require_relative 'module'
4
4
 
5
5
  module Scan
6
+ # rubocop:disable Metrics/ClassLength
6
7
  class Options
7
8
  def self.verify_type(item_name, acceptable_types, value)
8
9
  type_ok = [Array, String].any? { |type| value.kind_of?(type) }
@@ -322,6 +323,24 @@ module Scan
322
323
  env_name: "SCAN_SLACK_MESSAGE",
323
324
  description: "The message included with each message posted to slack",
324
325
  optional: true),
326
+ FastlaneCore::ConfigItem.new(key: :slack_use_webhook_configured_username_and_icon,
327
+ env_name: "SCAN_SLACK_USE_WEBHOOK_CONFIGURED_USERNAME_AND_ICON",
328
+ description: "Use webhook's default username and icon settings? (true/false)",
329
+ default_value: false,
330
+ type: Boolean,
331
+ optional: true),
332
+ FastlaneCore::ConfigItem.new(key: :slack_username,
333
+ env_name: "SCAN_SLACK_USERNAME",
334
+ description: "Overrides the webhook's username property if slack_use_webhook_configured_username_and_icon is false",
335
+ default_value: "fastlane",
336
+ is_string: true,
337
+ optional: true),
338
+ FastlaneCore::ConfigItem.new(key: :slack_icon_url,
339
+ env_name: "SCAN_SLACK_ICON_URL",
340
+ description: "Overrides the webhook's image property if slack_use_webhook_configured_username_and_icon is false",
341
+ default_value: "https://s3-eu-west-1.amazonaws.com/fastlane.tools/fastlane.png",
342
+ is_string: true,
343
+ optional: true),
325
344
  FastlaneCore::ConfigItem.new(key: :skip_slack,
326
345
  description: "Don't publish to slack, even when an URL is given",
327
346
  is_string: false,
@@ -349,4 +368,5 @@ module Scan
349
368
  ]
350
369
  end
351
370
  end
371
+ # rubocop:enable Metrics/ClassLength
352
372
  end
@@ -15,6 +15,8 @@ module Scan
15
15
  channel = ('#' + channel) unless ['#', '@'].include?(channel[0]) # send message to channel by default
16
16
  end
17
17
 
18
+ username = Scan.config[:slack_use_webhook_configured_username_and_icon] ? nil : Scan.config[:slack_username]
19
+ icon_url = Scan.config[:slack_use_webhook_configured_username_and_icon] ? nil : Scan.config[:slack_icon_url]
18
20
  fields = []
19
21
 
20
22
  if results[:build_errors]
@@ -46,8 +48,8 @@ module Scan
46
48
  channel: channel,
47
49
  slack_url: Scan.config[:slack_url].to_s,
48
50
  success: results[:build_errors].to_i == 0 && results[:failures].to_i == 0,
49
- username: 'fastlane',
50
- icon_url: 'https://s3-eu-west-1.amazonaws.com/fastlane.tools/fastlane.png',
51
+ username: username,
52
+ icon_url: icon_url,
51
53
  payload: {},
52
54
  attachment_properties: {
53
55
  fields: fields
@@ -199,13 +199,16 @@ module Sigh
199
199
  true
200
200
  end
201
201
 
202
- unless Sigh.config[:skip_certificate_verification]
203
- certificates = certificates.find_all do |c|
204
- file = Tempfile.new('cert')
205
- file.write(c.download_raw)
206
- file.close
207
-
208
- FastlaneCore::CertChecker.installed?(file.path)
202
+ # verify certificates
203
+ if Helper.mac?
204
+ unless Sigh.config[:skip_certificate_verification]
205
+ certificates = certificates.find_all do |c|
206
+ file = Tempfile.new('cert')
207
+ file.write(c.download_raw)
208
+ file.close
209
+
210
+ FastlaneCore::CertChecker.installed?(file.path)
211
+ end
209
212
  end
210
213
  end
211
214
 
@@ -221,7 +221,7 @@ module Spaceship
221
221
  end
222
222
 
223
223
  if ENV["DEBUG"]
224
- puts("To run _spaceship_ through a local proxy, use SPACESHIP_DEBUG")
224
+ puts("To run spaceship through a local proxy, use SPACESHIP_DEBUG")
225
225
  end
226
226
  end
227
227
  end
@@ -394,7 +394,7 @@ module Spaceship
394
394
  end
395
395
 
396
396
  # This method is used for both the Apple Dev Portal and App Store Connect
397
- # This will also handle 2 step verification
397
+ # This will also handle 2 step verification and 2 factor authentication
398
398
  #
399
399
  # It is called in `send_login_request` of sub classes (which the method `login`, above, transferred over to via `do_login`)
400
400
  def send_shared_login_request(user, password)
@@ -25,7 +25,6 @@ require_relative 'app_trailer'
25
25
  require_relative 'sandbox_tester'
26
26
  require_relative 'app_details'
27
27
  require_relative 'pricing_tier'
28
- require_relative 'recovery_device'
29
28
  require_relative 'territory'
30
29
  require_relative 'availability'
31
30
  require_relative 'members'
@@ -163,14 +163,13 @@ module Spaceship
163
163
  return unless raw.kind_of?(Hash)
164
164
 
165
165
  data = raw['data'] || raw # sometimes it's with data, sometimes it isn't
166
- error_keys_to_check = [
167
- "sectionErrorKeys",
168
- "sectionInfoKeys",
169
- "sectionWarningKeys",
170
- "validationErrors"
171
- ]
172
- errors_in_data = fetch_errors_in_data(data_section: data, keys: error_keys_to_check)
173
- errors_in_version_info = fetch_errors_in_data(data_section: data, sub_section_name: "versionInfo", keys: error_keys_to_check)
166
+
167
+ error_keys = ["sectionErrorKeys", "validationErrors", "serviceErrors"]
168
+ info_keys = ["sectionInfoKeys", "sectionWarningKeys"]
169
+ error_and_info_keys_to_check = error_keys + info_keys
170
+
171
+ errors_in_data = fetch_errors_in_data(data_section: data, keys: error_and_info_keys_to_check)
172
+ errors_in_version_info = fetch_errors_in_data(data_section: data, sub_section_name: "versionInfo", keys: error_and_info_keys_to_check)
174
173
 
175
174
  # If we have any errors or "info" we need to treat them as warnings or errors
176
175
  if errors_in_data.count == 0 && errors_in_version_info.count == 0
@@ -205,7 +204,6 @@ module Spaceship
205
204
  errors = handle_response_hash.call(data)
206
205
 
207
206
  # Search at data level, as well as "versionInfo" level for errors
208
- error_keys = ["sectionErrorKeys", "validationErrors"]
209
207
  errors_in_data = fetch_errors_in_data(data_section: data, keys: error_keys)
210
208
  errors_in_version_info = fetch_errors_in_data(data_section: data, sub_section_name: "versionInfo", keys: error_keys)
211
209
 
@@ -234,7 +232,6 @@ module Spaceship
234
232
  end
235
233
 
236
234
  # Search at data level, as well as "versionInfo" level for info and warnings
237
- info_keys = ["sectionInfoKeys", "sectionWarningKeys"]
238
235
  info_in_data = fetch_errors_in_data(data_section: data, keys: info_keys)
239
236
  info_in_version_info = fetch_errors_in_data(data_section: data, sub_section_name: "versionInfo", keys: info_keys)
240
237
 
@@ -1,19 +1,14 @@
1
1
  require_relative 'globals'
2
2
  require_relative 'tunes/tunes_client'
3
- require_relative 'tunes/recovery_device'
4
3
 
5
4
  module Spaceship
6
5
  class Client
7
6
  def handle_two_step_or_factor(response)
7
+ # extract `x-apple-id-session-id` and `scnt` from response, to be used by `update_request_headers`
8
8
  @x_apple_id_session_id = response["x-apple-id-session-id"]
9
9
  @scnt = response["scnt"]
10
10
 
11
- puts("")
12
- puts("Two-step Verification (4 digits code) or Two-factor Authentication (6 digits code) is enabled for account '#{self.user}'")
13
- puts("More information about Two-step Verification (4 digits code): https://support.apple.com/en-us/HT204152")
14
- puts("More information about Two-factor Authentication (6 digits code): https://support.apple.com/en-us/HT204915")
15
- puts("")
16
-
11
+ # get authentication options
17
12
  r = request(:get) do |req|
18
13
  req.url("https://idmsa.apple.com/appleauth/auth")
19
14
  update_request_headers(req)
@@ -28,35 +23,28 @@ module Spaceship
28
23
  end
29
24
  end
30
25
 
31
- def handle_two_step(r)
32
- if r.body.fetch("securityCode", {})["tooManyCodesLock"].to_s.length > 0
26
+ def handle_two_step(response)
27
+ if response.body.fetch("securityCode", {})["tooManyCodesLock"].to_s.length > 0
33
28
  raise Tunes::Error.new, "Too many verification codes have been sent. Enter the last code you received, use one of your devices, or try again later."
34
29
  end
35
30
 
36
- old_client = (begin
37
- Tunes::RecoveryDevice.client
38
- rescue
39
- nil # since client might be nil, which raises an exception
40
- end)
41
- Tunes::RecoveryDevice.client = self # temporary set it as it's required by the factory method
42
- devices = r.body["trustedDevices"].collect do |current|
43
- Tunes::RecoveryDevice.factory(current)
44
- end
45
- Tunes::RecoveryDevice.client = old_client
46
-
47
31
  puts("Two-step Verification (4 digits code) is enabled for account '#{self.user}'")
48
- puts("Please select a device to verify your identity")
49
- available = devices.collect do |c|
50
- "#{c.name}\t#{c.model_name || 'SMS'}\t(#{c.device_id})"
32
+ puts("More information about Two-step Verification: https://support.apple.com/en-us/HT204152")
33
+ puts("")
34
+
35
+ puts("Please select a trusted device to verify your identity")
36
+ available = response.body["trustedDevices"].collect do |current|
37
+ "#{current['name']}\t#{current['modelName'] || 'SMS'}\t(#{current['id']})"
51
38
  end
52
39
  result = choose(*available)
40
+
53
41
  device_id = result.match(/.*\t.*\t\((.*)\)/)[1]
54
- select_device(r, device_id)
42
+ handle_two_step_for_device(device_id)
55
43
  end
56
44
 
57
45
  # this is extracted into its own method so it can be called multiple times (see end)
58
- def select_device(r, device_id)
59
- # Request Token
46
+ def handle_two_step_for_device(device_id)
47
+ # Request token to device
60
48
  r = request(:put) do |req|
61
49
  req.url("https://idmsa.apple.com/appleauth/auth/verify/device/#{device_id}/securitycode")
62
50
  update_request_headers(req)
@@ -70,7 +58,7 @@ module Spaceship
70
58
  code = ask("Please enter the 4 digit code: ")
71
59
  puts("Requesting session...")
72
60
 
73
- # Send token back to server to get a valid session
61
+ # Send token to server to get a valid session
74
62
  r = request(:post) do |req|
75
63
  req.url("https://idmsa.apple.com/appleauth/auth/verify/device/#{device_id}/securitycode")
76
64
  req.headers['Content-Type'] = 'application/json'
@@ -101,7 +89,7 @@ module Spaceship
101
89
  # }
102
90
  if ex.to_s.include?("verification code") # to have a nicer output
103
91
  puts("Error: Incorrect verification code")
104
- return select_device(r, device_id)
92
+ return handle_two_step_for_device(device_id)
105
93
  end
106
94
 
107
95
  raise ex
@@ -112,29 +100,100 @@ module Spaceship
112
100
  return true
113
101
  end
114
102
 
115
- def handle_two_factor(response)
116
- two_factor_url = "https://github.com/fastlane/fastlane/tree/master/spaceship#2-step-verification"
117
- puts("Two-factor Authentication (6 digits code) is enabled for account '#{self.user}'")
103
+ def handle_two_factor(response, depth = 0)
104
+ if depth == 0
105
+ puts("Two-factor Authentication (6 digits code) is enabled for account '#{self.user}'")
106
+ puts("More information about Two-factor Authentication: https://support.apple.com/en-us/HT204915")
107
+ puts("")
108
+
109
+ two_factor_url = "https://github.com/fastlane/fastlane/tree/master/spaceship#2-step-verification"
110
+ puts("If you're running this in a non-interactive session (e.g. server or CI)")
111
+ puts("check out #{two_factor_url}")
112
+ end
118
113
 
119
- puts("If you're running this in a non-interactive session (e.g. server or CI)")
120
- puts("check out #{two_factor_url}")
114
+ # "verification code" has already be pushed to devices
121
115
 
122
116
  security_code = response.body["securityCode"]
123
- # securityCode =
124
- # {"length"=>6,
125
- # "tooManyCodesSent"=>false,
126
- # "tooManyCodesValidated"=>false,
127
- # "securityCodeLocked"=>false}
117
+ # "securityCode": {
118
+ # "length": 6,
119
+ # "tooManyCodesSent": false,
120
+ # "tooManyCodesValidated": false,
121
+ # "securityCodeLocked": false
122
+ # },
128
123
  code_length = security_code["length"]
129
- code = ask("Please enter the #{code_length} digit code: ")
124
+
125
+ puts("")
126
+ puts("(Input `sms` to escape this prompt and select a trusted phone number to send the code as a text message)")
127
+ code_type = 'trusteddevice'
128
+ code = ask("Please enter the #{code_length} digit code:")
129
+ body = { "securityCode" => { "code" => code.to_s } }.to_json
130
+
131
+ if code == 'sms'
132
+ code_type = 'phone'
133
+ body = request_two_factor_code_from_phone(response.body["trustedPhoneNumbers"], code_length)
134
+ end
135
+
130
136
  puts("Requesting session...")
131
137
 
132
- # Send securityCode back to server to get a valid session
138
+ # Send "verification code" back to server to get a valid session
133
139
  r = request(:post) do |req|
134
- req.url("https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode")
140
+ req.url("https://idmsa.apple.com/appleauth/auth/verify/#{code_type}/securitycode")
135
141
  req.headers['Content-Type'] = 'application/json'
136
- req.body = { "securityCode" => { "code" => code.to_s } }.to_json
142
+ req.body = body
143
+ update_request_headers(req)
144
+ end
145
+
146
+ begin
147
+ # we use `Spaceship::TunesClient.new.handle_itc_response`
148
+ # since this might be from the Dev Portal, but for 2 factor
149
+ Spaceship::TunesClient.new.handle_itc_response(r.body) # this will fail if the code is invalid
150
+ rescue => ex
151
+ # If the code was entered wrong
152
+ # {
153
+ # "service_errors": [{
154
+ # "code": "-21669",
155
+ # "title": "Incorrect Verification Code",
156
+ # "message": "Incorrect verification code."
157
+ # }],
158
+ # "hasError": true
159
+ # }
137
160
 
161
+ if ex.to_s.include?("verification code") # to have a nicer output
162
+ puts("Error: Incorrect verification code")
163
+ depth += 1
164
+ return handle_two_factor(response, depth)
165
+ end
166
+
167
+ raise ex
168
+ end
169
+
170
+ store_session
171
+
172
+ return true
173
+ end
174
+
175
+ def get_id_for_number(phone_numbers, result)
176
+ phone_numbers.each do |phone|
177
+ phone_id = phone['id']
178
+ return phone_id if phone['numberWithDialCode'] == result
179
+ end
180
+ end
181
+
182
+ def request_two_factor_code_from_phone(phone_numbers, code_length)
183
+ puts("Please select a trusted phone number to send code to:")
184
+
185
+ available = phone_numbers.collect do |current|
186
+ current['numberWithDialCode']
187
+ end
188
+ result = choose(*available)
189
+
190
+ phone_id = get_id_for_number(phone_numbers, result)
191
+
192
+ # Request code
193
+ r = request(:put) do |req|
194
+ req.url("https://idmsa.apple.com/appleauth/auth/verify/phone")
195
+ req.headers['Content-Type'] = 'application/json'
196
+ req.body = { "phoneNumber" => { "id" => phone_id }, "mode" => "sms" }.to_json
138
197
  update_request_headers(req)
139
198
  end
140
199
 
@@ -142,9 +201,10 @@ module Spaceship
142
201
  # since this might be from the Dev Portal, but for 2 step
143
202
  Spaceship::TunesClient.new.handle_itc_response(r.body)
144
203
 
145
- store_session
204
+ puts("Successfully requested text message")
146
205
 
147
- return true
206
+ code = ask("Please enter the #{code_length} digit code you received at #{result}:")
207
+ { "securityCode" => { "code" => code.to_s }, "phoneNumber" => { "id" => phone_id }, "mode" => "sms" }.to_json
148
208
  end
149
209
 
150
210
  def store_session
@@ -162,7 +222,6 @@ module Spaceship
162
222
 
163
223
  request(:get) do |req|
164
224
  req.url("https://idmsa.apple.com/appleauth/auth/2sv/trust")
165
-
166
225
  update_request_headers(req)
167
226
  end
168
227
  # This request will fail if the user isn't added to a team on iTC
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.115.0.beta.20190119200019
4
+ version: 2.115.0.beta.20190120200101
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danielle Tomlinson
@@ -27,7 +27,7 @@ authors:
27
27
  autorequire:
28
28
  bindir: bin
29
29
  cert_chain: []
30
- date: 2019-01-19 00:00:00.000000000 Z
30
+ date: 2019-01-20 00:00:00.000000000 Z
31
31
  dependencies:
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: slack-notifier
@@ -1657,7 +1657,6 @@ files:
1657
1657
  - spaceship/lib/spaceship/tunes/members.rb
1658
1658
  - spaceship/lib/spaceship/tunes/pricing_info.rb
1659
1659
  - spaceship/lib/spaceship/tunes/pricing_tier.rb
1660
- - spaceship/lib/spaceship/tunes/recovery_device.rb
1661
1660
  - spaceship/lib/spaceship/tunes/sandbox_tester.rb
1662
1661
  - spaceship/lib/spaceship/tunes/spaceship.rb
1663
1662
  - spaceship/lib/spaceship/tunes/territory.rb
@@ -1688,24 +1687,24 @@ metadata:
1688
1687
  post_install_message:
1689
1688
  rdoc_options: []
1690
1689
  require_paths:
1691
- - cert/lib
1692
- - spaceship/lib
1693
- - scan/lib
1690
+ - supply/lib
1691
+ - precheck/lib
1692
+ - pem/lib
1694
1693
  - fastlane/lib
1694
+ - spaceship/lib
1695
1695
  - snapshot/lib
1696
- - pem/lib
1697
- - gym/lib
1698
- - credentials_manager/lib
1699
- - screengrab/lib
1700
- - pilot/lib
1701
1696
  - match/lib
1697
+ - pilot/lib
1702
1698
  - sigh/lib
1703
- - fastlane_core/lib
1704
- - supply/lib
1705
- - deliver/lib
1706
- - precheck/lib
1699
+ - screengrab/lib
1707
1700
  - frameit/lib
1701
+ - fastlane_core/lib
1702
+ - cert/lib
1703
+ - credentials_manager/lib
1708
1704
  - produce/lib
1705
+ - gym/lib
1706
+ - deliver/lib
1707
+ - scan/lib
1709
1708
  required_ruby_version: !ruby/object:Gem::Requirement
1710
1709
  requirements:
1711
1710
  - - ">="
@@ -1,65 +0,0 @@
1
- require_relative 'tunes_base'
2
-
3
- module Spaceship
4
- module Tunes
5
- class RecoveryDevice < TunesBase
6
- # @return (String) ID provided by Apple
7
- # @example
8
- # "1801231651"
9
- attr_accessor :device_id
10
-
11
- # @return (String) The name of the device
12
- # @example
13
- # "Felix Krause's iPhone 6"
14
- attr_accessor :name
15
-
16
- # @return (Bool) This device looks suspicious [add emoji here]
17
- # this will probably always be true, otherwise the device
18
- # doesn't show up
19
- # @example
20
- # true
21
- attr_accessor :trusted
22
-
23
- # @return (Bool)
24
- # @example
25
- # true
26
- attr_accessor :status
27
-
28
- # @return (String) Remote URL to an image representing this device
29
- # This shows the attention to detail by Apple <3
30
- # @example
31
- # "https://appleid.cdn-apple.com/static/deviceImages-5.0/iPhone/iPhone8,1-e4e7e8-dadcdb/online-sourcelist__3x.png"
32
- # @example
33
- # "https://appleid.cdn-apple.com/appleauth/static/bin/cb2613252489/images/sms@3x.png"
34
- attr_accessor :device_image
35
-
36
- # @return (String)
37
- # @example
38
- # "iPad Air"
39
- # @example
40
- # nil # e.g. when it's a phone number
41
- attr_accessor :model_name
42
-
43
- # @return (String)
44
- # @example
45
- # "79"
46
- attr_accessor :last_two_digits
47
-
48
- # @return (Number)
49
- # @example
50
- # 1446488271926
51
- attr_accessor :update_date
52
-
53
- attr_mapping(
54
- 'id' => :device_id,
55
- 'name' => :name,
56
- 'trusted' => :trusted,
57
- 'status' => :status,
58
- 'imageLocation3x' => :device_image,
59
- 'modelName' => :model_name,
60
- 'lastTwoDigits' => :last_two_digits,
61
- 'updateDate' => :update_date
62
- )
63
- end
64
- end
65
- end