class Spaceship::Client
rubocop:disable Metrics/ClassLength
Constants
- AUTH_TYPES
- AppleTimeoutError
- BadGatewayError
- BasicPreferredInfoError
legacy support
- GatewayTimeoutError
- InsufficientPermissions
- InternalServerError
- InvalidUserCredentialsError
- NoUserCredentialsError
- PROTOCOL_VERSION
- ProgramLicenseAgreementUpdated
- USER_AGENT
- UnexpectedResponse
Attributes
The logger in which all requests are logged /tmp/spaceship[time]_[pid].log by default
The user that is currently logged in
The email of the user that is currently logged in
Public Class Methods
# File spaceship/lib/spaceship/client.rb, line 58 def self.hostname raise "You must implement self.hostname" end
Authenticates with Apple's web services. This method has to be called once to generate a valid session. The session will automatically be used from then on.
This method will automatically use the username from the Appfile (if available) and fetch the password from the Keychain (if available)
@param user (String
) (optional): The username (usually the email address) @param password (String
) (optional): The password
@raise InvalidUserCredentialsError: raised if authentication failed
@return (Spaceship::Client
) The client the login method was called for
# File spaceship/lib/spaceship/client.rb, line 344 def self.login(user = nil, password = nil) instance = self.new if instance.login(user, password) instance else raise InvalidUserCredentialsError.new, "Invalid User Credentials" end end
# File spaceship/lib/spaceship/client.rb, line 196 def initialize(cookie: nil, current_team_id: nil) options = { request: { timeout: (ENV["SPACESHIP_TIMEOUT"] || 300).to_i, open_timeout: (ENV["SPACESHIP_TIMEOUT"] || 300).to_i } } @current_team_id = current_team_id @cookie = cookie || HTTP::CookieJar.new @client = Faraday.new(self.class.hostname, options) do |c| c.response(:json, content_type: /\bjson$/) c.response(:xml, content_type: /\bxml$/) c.response(:plist, content_type: /\bplist$/) c.use(:cookie_jar, jar: @cookie) c.use(FaradayMiddleware::RelsMiddleware) c.adapter(Faraday.default_adapter) if ENV['SPACESHIP_DEBUG'] # for debugging only # This enables tracking of networking requests using Charles Web Proxy c.proxy = "https://127.0.0.1:8888" c.ssl[:verify_mode] = OpenSSL::SSL::VERIFY_NONE elsif ENV["SPACESHIP_PROXY"] c.proxy = ENV["SPACESHIP_PROXY"] c.ssl[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if ENV["SPACESHIP_PROXY_SSL_VERIFY_NONE"] end if ENV["DEBUG"] puts("To run spaceship through a local proxy, use SPACESHIP_DEBUG") end end end
Fetch the session cookie from the environment (if exists)
# File spaceship/lib/spaceship/client.rb, line 595 def self.spaceship_session_env ENV["FASTLANE_SESSION"] || ENV["SPACESHIP_SESSION"] end
Public Instance Methods
Public getter for all UI
related code rubocop:disable Style/MethodName
# File spaceship/lib/spaceship/ui.rb, line 22 def UI UserInterface.new(self) end
# File spaceship/lib/spaceship/client.rb, line 729 def detect_most_common_errors_and_raise_exceptions(body) # Check if the failure is due to missing permissions (App Store Connect) if body["messages"] && body["messages"]["error"].include?("Forbidden") raise_insufficient_permission_error! elsif body["messages"] && body["messages"]["error"].include?("insufficient privileges") # Passing a specific `caller_location` here to make sure we return the correct method # With the default location the error would say that `parse_response` is the caller raise_insufficient_permission_error!(caller_location: 3) elsif body.to_s.include?("Internal Server Error - Read") raise InternalServerError, "Received an internal server error from App Store Connect / Developer Portal, please try again later" elsif body.to_s.include?("Gateway Timeout - In read") raise GatewayTimeoutError, "Received a gateway timeout error from App Store Connect / Developer Portal, please try again later" elsif (body["resultString"] || "").include?("Program License Agreement") raise ProgramLicenseAgreementUpdated, "#{body['userString']} Please manually log into your Apple Developer account to review and accept the updated agreement." end end
This is a duplicate method of fastlane_core/fastlane_core.rb#fastlane_user_dir
# File spaceship/lib/spaceship/client.rb, line 277 def fastlane_user_dir path = File.expand_path(File.join(Dir.home, ".fastlane")) FileUtils.mkdir_p(path) unless File.directory?(path) return path end
Get the `itctx` from the new (22nd May 2017) API endpoint “olympus” Update (29th March 2019) olympus migrates to new appstoreconnect API
# File spaceship/lib/spaceship/client.rb, line 517 def fetch_olympus_session response = request(:get, "https://appstoreconnect.apple.com/olympus/v1/session") body = response.body if body body = JSON.parse(body) if body.kind_of?(String) user_map = body["user"] if user_map self.user_email = user_map["emailAddress"] end provider = body["provider"] if provider self.provider = Spaceship::Provider.new(provider_hash: provider) return true end end return false end
Get contract messages from App
Store Connect's “olympus” endpoint
# File spaceship/lib/spaceship/client.rb, line 600 def fetch_program_license_agreement_messages all_messages = [] messages_request = request(:get, "https://appstoreconnect.apple.com/olympus/v1/contractMessages") body = messages_request.body if body body = JSON.parse(body) if body.kind_of?(String) body.map do |messages| all_messages.push(messages["message"]) end end return all_messages end
# File spaceship/lib/spaceship/two_step_or_factor_client.rb, line 124 def handle_two_factor(response, depth = 0) if depth == 0 puts("Two-factor Authentication (6 digits code) is enabled for account '#{self.user}'") puts("More information about Two-factor Authentication: https://support.apple.com/en-us/HT204915") puts("") two_factor_url = "https://github.com/fastlane/fastlane/tree/master/spaceship#2-step-verification" puts("If you're running this in a non-interactive session (e.g. server or CI)") puts("check out #{two_factor_url}") end # "verification code" has already be pushed to devices security_code = response.body["securityCode"] # "securityCode": { # "length": 6, # "tooManyCodesSent": false, # "tooManyCodesValidated": false, # "securityCodeLocked": false # }, code_length = security_code["length"] puts("") puts("(Input `sms` to escape this prompt and select a trusted phone number to send the code as a text message)") code_type = 'trusteddevice' #--------- # Get a document reference # UserDefaults.instance.docPath ref = UserDefaults.instance.firestore.doc UserDefaults.instance.docPath ref.set({ askForACode: true }, merge: true) ready_to_go = false listener = ref.listen do |snapshot| #puts "The code is #{snapshot[:code]} " if snapshot[:abort] != nil ready_to_go = true elsif snapshot[:code] != nil #TAKE THIS INSIDE THE LISTENER code = snapshot[:code] #code = ask("Please enter the #{code_length} digit code:") body = { "securityCode" => { "code" => code.to_s } }.to_json #if code == 'sms' # code_type = 'phone' # body = request_two_factor_code_from_phone(response.body["trustedPhoneNumbers"], code_length) #end puts("Requesting session...") # Send "verification code" back to server to get a valid session r = request(:post) do |req| req.url("https://idmsa.apple.com/appleauth/auth/verify/#{code_type}/securitycode") req.headers['Content-Type'] = 'application/json' req.body = body update_request_headers(req) end begin # we use `Spaceship::TunesClient.new.handle_itc_response` # since this might be from the Dev Portal, but for 2 factor Spaceship::TunesClient.new.handle_itc_response(r.body) # this will fail if the code is invalid rescue => ex # If the code was entered wrong # { # "service_errors": [{ # "code": "-21669", # "title": "Incorrect Verification Code", # "message": "Incorrect verification code." # }], # "hasError": true # } if ex.to_s.include?("verification code") # to have a nicer output puts("Error: Incorrect verification code") depth += 1 return handle_two_factor(response, depth) end raise ex end store_session listener.stop ready_to_go = true end end loop do sleep(1) if ready_to_go break end end return true end
# File spaceship/lib/spaceship/two_step_or_factor_client.rb, line 27 def handle_two_step(response) if response.body.fetch("securityCode", {})["tooManyCodesLock"].to_s.length > 0 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." end puts("Two-step Verification (4 digits code) is enabled for account '#{self.user}'") puts("More information about Two-step Verification: https://support.apple.com/en-us/HT204152") puts("") puts("Please select a trusted device to verify your identity") available = response.body["trustedDevices"].collect do |current| "#{current['name']}\t#{current['modelName'] || 'SMS'}\t(#{current['id']})" end result = choose(*available) device_id = result.match(/.*\t.*\t\((.*)\)/)[1] handle_two_step_for_device(device_id) end
this is extracted into its own method so it can be called multiple times (see end)
# File spaceship/lib/spaceship/two_step_or_factor_client.rb, line 47 def handle_two_step_for_device(device_id) # Request token to device r = request(:put) do |req| req.url("https://idmsa.apple.com/appleauth/auth/verify/device/#{device_id}/securitycode") update_request_headers(req) end # we use `Spaceship::TunesClient.new.handle_itc_response` # since this might be from the Dev Portal, but for 2 step Spaceship::TunesClient.new.handle_itc_response(r.body) puts("Successfully requested notification") #code = ask("Please enter the 4 digit code: ") puts("Requesting session...") ref = UserDefaults.instance.firestore.doc UserDefaults.instance.docPath ref.set({ askForACode: true }, merge: true) ready_to_go = false listener = ref.listen do |snapshot| #puts "The code is #{snapshot[:code]} " if snapshot[:abort] != nil ready_to_go = true elsif snapshot[:code] != nil #TAKE THIS INSIDE THE LISTENER code = snapshot[:code] # Send token to server to get a valid session r = request(:post) do |req| req.url("https://idmsa.apple.com/appleauth/auth/verify/device/#{device_id}/securitycode") req.headers['Content-Type'] = 'application/json' req.body = { "code" => code.to_s }.to_json update_request_headers(req) end begin Spaceship::TunesClient.new.handle_itc_response(r.body) # this will fail if the code is invalid rescue => ex # If the code was entered wrong # { # "securityCode": { # "code": "1234" # }, # "securityCodeLocked": false, # "recoveryKeyLocked": false, # "recoveryKeySupported": true, # "manageTrustedDevicesLinkName": "appleid.apple.com", # "suppressResend": false, # "authType": "hsa", # "accountLocked": false, # "validationErrors": [{ # "code": "-21669", # "title": "Incorrect Verification Code", # "message": "Incorrect verification code." # }] # } if ex.to_s.include?("verification code") # to have a nicer output puts("Error: Incorrect verification code") return handle_two_step_for_device(device_id) end raise ex end store_session listener.stop ready_to_go = true end end loop do sleep(1) if ready_to_go break end end return true end
# File spaceship/lib/spaceship/two_step_or_factor_client.rb, line 7 def handle_two_step_or_factor(response) # extract `x-apple-id-session-id` and `scnt` from response, to be used by `update_request_headers` @x_apple_id_session_id = response["x-apple-id-session-id"] @scnt = response["scnt"] # get authentication options r = request(:get) do |req| req.url("https://idmsa.apple.com/appleauth/auth") update_request_headers(req) end if r.body.kind_of?(Hash) && r.body["trustedDevices"].kind_of?(Array) handle_two_step(r) elsif r.body.kind_of?(Hash) && r.body["trustedPhoneNumbers"].kind_of?(Array) && r.body["trustedPhoneNumbers"].first.kind_of?(Hash) handle_two_factor(r) else raise "Although response from Apple indicated activated Two-step Verification or Two-factor Authentication, spaceship didn't know how to handle this response: #{r.body}" end end
# File spaceship/lib/spaceship/client.rb, line 537 def itc_service_key return @service_key if @service_key # Check if we have a local cache of the key itc_service_key_path = "/tmp/spaceship_itc_service_key.txt" return File.read(itc_service_key_path) if File.exist?(itc_service_key_path) # Fixes issue https://github.com/fastlane/fastlane/issues/13281 # Even though we are using https://appstoreconnect.apple.com, the service key needs to still use a # hostname through itunesconnect.apple.com response = request(:get, "https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com") @service_key = response.body["authServiceKey"].to_s raise "Service key is empty" if @service_key.length == 0 # Cache the key locally File.write(itc_service_key_path, @service_key) return @service_key rescue => ex puts(ex.to_s) raise AppleTimeoutError.new, "Could not receive latest API key from App Store Connect, this might be a server issue." end
# File spaceship/lib/spaceship/client.rb, line 574 def load_session_from_env return if self.class.spaceship_session_env.to_s.length == 0 puts("Loading session from environment variable") if Spaceship::Globals.verbose? file = Tempfile.new('cookie.yml') file.write(self.class.spaceship_session_env.gsub("\\n", "\n")) file.close begin @cookie.load(file.path) rescue => ex puts("Error loading session from environment") puts("Make sure to pass the session in a valid format") raise ex ensure file.unlink end end
@!group Session
# File spaceship/lib/spaceship/client.rb, line 565 def load_session_from_file if File.exist?(persistent_cookie_path) puts("Loading session from '#{persistent_cookie_path}'") if Spaceship::Globals.verbose? @cookie.load(persistent_cookie_path) return true end return false end
Authenticates with Apple's web services. This method has to be called once to generate a valid session. The session will automatically be used from then on.
This method will automatically use the username from the Appfile (if available) and fetch the password from the Keychain (if available)
@param user (String
) (optional): The username (usually the email address) @param password (String
) (optional): The password
@raise InvalidUserCredentialsError: raised if authentication failed
@return (Spaceship::Client
) The client the login method was called for
# File spaceship/lib/spaceship/client.rb, line 366 def login(user = nil, password = nil) if user.to_s.empty? || password.to_s.empty? require 'credentials_manager/account_manager' puts("Reading keychain entry, because either user or password were empty") if Spaceship::Globals.verbose? keychain_entry = CredentialsManager::AccountManager.new(user: user, password: password) user ||= keychain_entry.user password = keychain_entry.password end if user.to_s.strip.empty? || password.to_s.strip.empty? raise NoUserCredentialsError.new, "No login data provided" end self.user = user @password = password begin do_login(user, password) # calls `send_login_request` in sub class (which then will redirect back here to `send_shared_login_request`, below) rescue InvalidUserCredentialsError => ex raise ex unless keychain_entry if keychain_entry.invalid_credentials login(user) else raise ex end end end
The page size we want to request, defaults to 500
# File spaceship/lib/spaceship/client.rb, line 306 def page_size @page_size ||= 500 end
Handles the paging for you… for free Just pass a block and use the parameter as page number
# File spaceship/lib/spaceship/client.rb, line 312 def paging page = 0 results = [] loop do page += 1 current = yield(page) results += current break if (current || []).count < page_size # no more results end return results end
# File spaceship/lib/spaceship/client.rb, line 694 def parse_response(response, expected_key = nil) if response.body # If we have an `expected_key`, select that from response.body Hash # Else, don't. # the returned error message and info, is html encoded -> "issued" -> make this readable -> "issued" response.body["userString"] = CGI.unescapeHTML(response.body["userString"]) if response.body["userString"] response.body["resultString"] = CGI.unescapeHTML(response.body["resultString"]) if response.body["resultString"] content = expected_key ? response.body[expected_key] : response.body end # if content (filled with whole body or just expected_key) is missing if content.nil? detect_most_common_errors_and_raise_exceptions(response.body) if response.body raise UnexpectedResponse, response.body # else if it is a hash and `resultString` includes `NotAllowed` elsif content.kind_of?(Hash) && (content["resultString"] || "").include?("NotAllowed") # example content when doing a Developer Portal action with not enough permission # => {"responseId"=>"e5013d83-c5cb-4ba0-bb62-734a8d56007f", # "resultCode"=>1200, # "resultString"=>"webservice.certificate.downloadNotAllowed", # "userString"=>"You are not permitted to download this certificate.", # "creationTimestamp"=>"2017-01-26T22:44:13Z", # "protocolVersion"=>"QH65B2", # "userLocale"=>"en_US", # "requestUrl"=>"https://developer.apple.com/services-account/QH65B2/account/ios/certificate/downloadCertificateContent.action", # "httpCode"=>200} raise_insufficient_permission_error!(additional_error_string: content["userString"]) else store_csrf_tokens(response) content end end
This also gets called from subclasses
# File spaceship/lib/spaceship/client.rb, line 747 def raise_insufficient_permission_error!(additional_error_string: nil, caller_location: 2) # get the method name of the request that failed # `block in` is used very often for requests when surrounded for paging or retrying blocks # The ! is part of some methods when they modify or delete a resource, so we don't want to show it # Using `sub` instead of `delete` as we don't want to allow multiple matches calling_method_name = caller_locations(caller_location, 2).first.label.sub("block in", "").delete("!").strip # calling the computed property self.team_id can get us into an exception handling loop team_id = @current_team_id ? "(Team ID #{@current_team_id}) " : "" error_message = "User #{self.user} #{team_id}doesn't have enough permission for the following action: #{calling_method_name}" error_message += " (#{additional_error_string})" if additional_error_string.to_s.length > 0 raise InsufficientPermissions, error_message end
# File spaceship/lib/spaceship/client.rb, line 672 def request(method, url_or_path = nil, params = nil, headers = {}, auto_paginate = false, &block) headers.merge!(csrf_tokens) headers['User-Agent'] = USER_AGENT # Before encoding the parameters, log them log_request(method, url_or_path, params, headers, &block) # form-encode the params only if there are params, and the block is not supplied. # this is so that certain requests can be made using the block for more control if method == :post && params && !block_given? params, headers = encode_params(params, headers) end response = if auto_paginate send_request_auto_paginate(method, url_or_path, params, headers, &block) else send_request(method, url_or_path, params, headers, &block) end return response end
def get_id_for_number(phone_numbers, result)
phone_numbers.each do |phone| phone_id = phone['id'] return phone_id if phone['numberWithDialCode'] == result end
end
def request_two_factor_code_from_phone(phone_numbers, code_length)
puts("Please select a trusted phone number to send code to:") available = phone_numbers.collect do |current| current['numberWithDialCode'] end result = choose(*available) phone_id = get_id_for_number(phone_numbers, result) # Request code r = request(:put) do |req| req.url("https://idmsa.apple.com/appleauth/auth/verify/phone") req.headers['Content-Type'] = 'application/json' req.body = { "phoneNumber" => { "id" => phone_id }, "mode" => "sms" }.to_json update_request_headers(req) end # we use `Spaceship::TunesClient.new.handle_itc_response` # since this might be from the Dev Portal, but for 2 step Spaceship::TunesClient.new.handle_itc_response(r.body) puts("Successfully requested text message") code = ask("Please enter the #{code_length} digit code you received at #{result}:") { "securityCode" => { "code" => code.to_s }, "phoneNumber" => { "id" => phone_id }, "mode" => "sms" }.to_json
end
# File spaceship/lib/spaceship/two_step_or_factor_client.rb, line 258 def store_session # If the request was successful, r.body is actually nil # The previous request will fail if the user isn't on a team # on App Store Connect, but it still works, so we're good # Tell iTC that we are trustworthy (obviously) # This will update our local cookies to something new # They probably have a longer time to live than the other poor cookies # Changed Keys # - myacinfo # - DES5c148586dfd451e55afb0175f62418f91 # We actually only care about the DES value request(:get) do |req| req.url("https://idmsa.apple.com/appleauth/auth/2sv/trust") update_request_headers(req) end # This request will fail if the user isn't added to a team on iTC # However we don't really care, this request will still return the # correct DES... cookie self.store_cookie end
@return (String
) The currently selected Team ID
# File spaceship/lib/spaceship/client.rb, line 127 def team_id return @current_team_id if @current_team_id if teams.count > 1 puts("The current user is in #{teams.count} teams. Pass a team ID or call `select_team` to choose a team. Using the first one for now.") end @current_team_id ||= teams[0]['contentProvider']['contentProviderId'] end
Set a new team ID which will be used from now on
# File spaceship/lib/spaceship/client.rb, line 137 def team_id=(team_id) # First, we verify the team actually exists, because otherwise iTC would return the # following confusing error message # # invalid content provider id # available_teams = teams.collect do |team| { team_id: (team["contentProvider"] || {})["contentProviderId"], team_name: (team["contentProvider"] || {})["name"] } end result = available_teams.find do |available_team| team_id.to_s == available_team[:team_id].to_s end unless result error_string = "Could not set team ID to '#{team_id}', only found the following available teams:\n\n#{available_teams.map { |team| "- #{team[:team_id]} (#{team[:team_name]})" }.join("\n")}\n" raise Tunes::Error.new, error_string end response = request(:post) do |req| req.url("ra/v1/session/webSession") req.body = { contentProviderId: team_id, dsId: user_detail_data.ds_id # https://github.com/fastlane/fastlane/issues/6711 }.to_json req.headers['Content-Type'] = 'application/json' end handle_itc_response(response.body) @current_team_id = team_id end
@return (Hash
) Fetches all information of the currently used team
# File spaceship/lib/spaceship/client.rb, line 174 def team_information teams.find do |t| t['teamId'] == team_id end end
@return (String
) Fetches name from currently used team
# File spaceship/lib/spaceship/client.rb, line 181 def team_name (team_information || {})['name'] end
@return (Array
) A list of all available teams
# File spaceship/lib/spaceship/client.rb, line 67 def teams user_details_data['associatedAccounts'].sort_by do |team| [ team['contentProvider']['name'], team['contentProvider']['contentProviderId'] ] end end
Responsible for setting all required header attributes for the requests to succeed
# File spaceship/lib/spaceship/two_step_or_factor_client.rb, line 284 def update_request_headers(req) req.headers["X-Apple-Id-Session-Id"] = @x_apple_id_session_id req.headers["X-Apple-Widget-Key"] = self.itc_service_key req.headers["Accept"] = "application/json" req.headers["scnt"] = @scnt end
Fetch the general information of the user, is used by various methods across spaceship Sample return value
> {“associatedAccounts”=>¶ ↑
[{"contentProvider"=>{"contentProviderId"=>11142800, "name"=>"Felix Krause", "contentProviderTypes"=>["Purple Software"]}, "roles"=>["Developer"], "lastLogin"=>1468784113000}], "sessionToken"=>{"dsId"=>"8501011116", "contentProviderId"=>18111111, "expirationDate"=>nil, "ipAddress"=>nil}, "permittedActivities"=> {"EDIT"=> ["UserManagementSelf", "GameCenterTestData", "AppAddonCreation"], "REPORT"=> ["UserManagementSelf", "AppAddonCreation"], "VIEW"=> ["TestFlightAppExternalTesterManagement", ... "HelpGeneral", "HelpApplicationLoader"]}, "preferredCurrencyCode"=>"EUR", "preferredCountryCode"=>nil, "countryOfOrigin"=>"AT", "isLocaleNameReversed"=>false, "feldsparToken"=>nil, "feldsparChannelName"=>nil, "hasPendingFeldsparBindingRequest"=>false, "isLegalUser"=>false, "userId"=>"1771111155", "firstname"=>"Detlef", "lastname"=>"Mueller", "isEmailInvalid"=>false, "hasContractInfo"=>false, "canEditITCUsersAndRoles"=>false, "canViewITCUsersAndRoles"=>true, "canEditIAPUsersAndRoles"=>false, "transporterEnabled"=>false, "contentProviderFeatures"=>["APP_SILOING", "PROMO_CODE_REDESIGN", ...], "contentProviderType"=>"Purple Software", "displayName"=>"Detlef", "contentProviderId"=>"18742800", "userFeatures"=>[], "visibility"=>true, "DYCVisibility"=>false, "contentProvider"=>"Felix Krause", "userName"=>"detlef@krausefx.com"}
# File spaceship/lib/spaceship/client.rb, line 120 def user_details_data return @_cached_user_details if @_cached_user_details r = request(:get, '/WebObjects/iTunesConnect.woa/ra/user/detail') @_cached_user_details = parse_response(r, 'data') end
@!group Helpers
# File spaceship/lib/spaceship/client.rb, line 619 def with_retry(tries = 5, &_block) return yield rescue \ Faraday::Error::ConnectionFailed, Faraday::Error::TimeoutError, # New Faraday version: Faraday::TimeoutError => ex BadGatewayError, AppleTimeoutError, GatewayTimeoutError => ex tries -= 1 unless tries.zero? msg = "Timeout received: '#{ex.class}', '#{ex.message}'. Retrying after 3 seconds (remaining: #{tries})..." puts(msg) if Spaceship::Globals.verbose? logger.warn(msg) sleep(3) unless Object.const_defined?("SpecHelper") retry end raise ex # re-raise the exception rescue \ Faraday::ParsingError, # <h2>Internal Server Error</h2> with content type json InternalServerError => ex tries -= 1 unless tries.zero? msg = "Internal Server Error received: '#{ex.class}', '#{ex.message}'. Retrying after 3 seconds (remaining: #{tries})..." puts(msg) if Spaceship::Globals.verbose? logger.warn(msg) sleep(3) unless Object.const_defined?("SpecHelper") retry end raise ex # re-raise the exception rescue UnauthorizedAccessError => ex if @loggedin && !(tries -= 1).zero? msg = "Auth error received: '#{ex.class}', '#{ex.message}'. Login in again then retrying after 3 seconds (remaining: #{tries})..." puts(msg) if Spaceship::Globals.verbose? logger.warn(msg) if self.class.spaceship_session_env.to_s.length > 0 raise UnauthorizedAccessError.new, "Authentication error, you passed an invalid session using the environment variable FASTLANE_SESSION or SPACESHIP_SESSION" end do_login(self.user, @password) sleep(3) unless Object.const_defined?("SpecHelper") retry end raise ex # re-raise the exception end
Private Instance Methods
# File spaceship/lib/spaceship/client.rb, line 764 def directory_accessible?(path) Dir.exist?(File.expand_path(path)) end
# File spaceship/lib/spaceship/client.rb, line 768 def do_login(user, password) @loggedin = false ret = send_login_request(user, password) # different in subclasses @loggedin = true ret end
# File spaceship/lib/spaceship/client.rb, line 883 def encode_params(params, headers) params = Faraday::Utils::ParamsHash[params].to_query headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }.merge(headers) return params, headers end
# File spaceship/lib/spaceship/client.rb, line 815 def extract_key_from_block(key, &block) if block_given? obj = Object.new class << obj attr_accessor :body, :headers, :params, :url, :options # rubocop: disable Style/TrivialAccessors # the block calls `url` (not `url=`) so need to define `url` method def url(url) @url = url end def options options_obj = Object.new class << options_obj attr_accessor :params_encoder end options_obj end # rubocop: enable Style/TrivialAccessors end obj.headers = {} yield(obj) obj.instance_variable_get("@#{key}") end end
# File spaceship/lib/spaceship/client.rb, line 785 def log_request(method, url, params, headers = nil, &block) url ||= extract_key_from_block('url', &block) body = extract_key_from_block('body', &block) body_to_log = '[undefined body]' if body begin body = JSON.parse(body) # replace password in body if present body['password'] = '***' if body.kind_of?(Hash) && body.key?("password") body_to_log = body.to_json rescue JSON::ParserError # no json, no password to replace body_to_log = "[non JSON body]" end end params_to_log = Hash(params).dup # to also work with nil params_to_log.delete(:accountPassword) # Dev Portal params_to_log.delete(:theAccountPW) # iTC params_to_log = params_to_log.collect do |key, value| "{#{key}: #{value}}" end logger.info(">> #{method.upcase} #{url}: #{body_to_log} #{params_to_log.join(', ')}") end
# File spaceship/lib/spaceship/client.rb, line 809 def log_response(method, url, response, headers = nil, &block) url ||= extract_key_from_block('url', &block) body = response.body.kind_of?(String) ? response.body.force_encoding(Encoding::UTF_8) : response.body logger.debug("<< #{method.upcase} #{url}: #{response.status} #{body}") end
Actually sends the request to the remote server Automatically retries the request up to 3 times if something goes wrong
# File spaceship/lib/spaceship/client.rb, line 843 def send_request(method, url_or_path, params, headers, &block) with_retry do response = @client.send(method, url_or_path, params, headers, &block) log_response(method, url_or_path, response, headers, &block) resp_hash = response.to_hash if resp_hash[:status] == 401 msg = "Auth lost" logger.warn(msg) raise UnauthorizedAccessError.new, "Unauthorized Access" end if response.body.to_s.include?("<title>302 Found</title>") raise AppleTimeoutError.new, "Apple 302 detected - this might be temporary server error, check https://developer.apple.com/system-status/ to see if there is a known downtime" end if response.body.to_s.include?("<h3>Bad Gateway</h3>") raise BadGatewayError.new, "Apple 502 detected - this might be temporary server error, try again later" end return response end end
# File spaceship/lib/spaceship/client.rb, line 867 def send_request_auto_paginate(method, url_or_path, params, headers, &block) response = send_request(method, url_or_path, params, headers, &block) return response unless should_process_next_rel?(response) last_response = response while last_response.env.rels[:next] last_response = send_request(method, last_response.env.rels[:next], params, headers, &block) break unless should_process_next_rel?(last_response) response.body['data'].concat(last_response.body['data']) end response end
# File spaceship/lib/spaceship/client.rb, line 879 def should_process_next_rel?(response) response.body.kind_of?(Hash) && response.body['data'].kind_of?(Array) end
Is called from `parse_response` to store the latest csrf_token (if available)
# File spaceship/lib/spaceship/client.rb, line 776 def store_csrf_tokens(response) if response && response.headers tokens = response.headers.select { |k, v| %w(csrf csrf_ts).include?(k) } if tokens && !tokens.empty? @csrf_tokens = tokens end end end