<commit_before>require 'faraday' # HTTP Client
require 'logger'
require 'faraday_middleware'
require 'faraday-cookie_jar'
require 'spaceship/ui'
require 'spaceship/helper/plist_middleware'
require 'spaceship/helper/net_http_generic_request'

Faraday::Utils.default_params_encoder = Faraday::FlatParamsEncoder

if ENV["DEBUG"]
  require 'openssl'
  # this has to be on top of this file, since the value can't be changed later
  OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
end

module Spaceship
  class Client
    PROTOCOL_VERSION = "QH65B2"
    USER_AGENT = "Spaceship #{Spaceship::VERSION}"

    attr_reader :client

    # The user that is currently logged in
    attr_accessor :user

    # The logger in which all requests are logged
    # /tmp/spaceship[time]_[pid].log by default
    attr_accessor :logger

    attr_accessor :csrf_tokens

    # Base class for errors that want to present their message as
    # preferred error info for fastlane error handling. See:
    # fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb
    class BasicPreferredInfoError < StandardError
      TITLE = 'The request could not be completed because:'.freeze

      def preferred_error_info
        message ? [TITLE, message] : nil
      end
    end

    # Invalid user credentials were provided
    class InvalidUserCredentialsError < BasicPreferredInfoError; end

    # Raised when no user credentials were passed at all
    class NoUserCredentialsError < BasicPreferredInfoError; end

    class UnexpectedResponse < StandardError
      attr_reader :error_info

      def initialize(error_info = nil)
        super(error_info)
        @error_info = error_info
      end

      def preferred_error_info
        return nil unless @error_info.kind_of?(Hash) && @error_info['resultString']

        [
          "Apple provided the following error info:",
          @error_info['resultString'],
          @error_info['userString']
        ].compact.uniq # sometimes 'resultString' and 'userString' are the same value
      end
    end

    # Raised when 302 is received from portal request
    class AppleTimeoutError < BasicPreferredInfoError; end

    # Raised when 401 is received from portal request
    class UnauthorizedAccessError < BasicPreferredInfoError; 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
    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

    def self.hostname
      raise "You must implemented self.hostname"
    end

    def initialize
      options = {
       request: {
          timeout:       300,
          open_timeout:  300
        }
      }
      @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.adapter Faraday.default_adapter

        if ENV['DEBUG']
          # for debugging only
          # This enables tracking of networking requests using Charles Web Proxy
          c.response :logger
          c.proxy "https://127.0.0.1:8888"
        end
      end
    end

    # The logger in which all requests are logged
    # /tmp/spaceship[time]_[pid].log by default
    def logger
      unless @logger
        if ENV["VERBOSE"]
          @logger = Logger.new(STDOUT)
        else
          # Log to file by default
          path = "/tmp/spaceship#{Time.now.to_i}_#{Process.pid}.log"
          @logger = Logger.new(path)
        end

        @logger.formatter = proc do |severity, datetime, progname, msg|
          "[#{datetime.strftime('%H:%M:%S')}]: #{msg}\n"
        end
      end

      @logger
    end

    ##
    # Return the session cookie.
    #
    # @return (String) the cookie-string in the RFC6265 format: https://tools.ietf.org/html/rfc6265#section-4.2.1
    def cookie
      @cookie.map(&:to_s).join(';')
    end

    def store_cookie(path: nil)
      path ||= persistent_cookie_path

      # really important to specify the session to true
      # otherwise myacinfo and more won't be stored
      @cookie.save(path, :yaml, session: true)
      return File.read(path)
    end

    def persistent_cookie_path
      path = File.expand_path(File.join("~", ".spaceship", self.user, "cookie"))
      FileUtils.mkdir_p(File.expand_path("..", path))
      return path
    end

    #####################################################
    # @!group Automatic Paging
    #####################################################

    # The page size we want to request, defaults to 500
    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
    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

    #####################################################
    # @!group Login and Team Selection
    #####################################################

    # 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
    def login(user = nil, password = nil)
      if user.to_s.empty? or password.to_s.empty?
        require 'credentials_manager'

        keychain_entry = CredentialsManager::AccountManager.new(user: user, password: password)
        user ||= keychain_entry.user
        password = keychain_entry.password
      end

      if user.to_s.strip.empty? or password.to_s.strip.empty?
        raise NoUserCredentialsError.new, "No login data provided"
      end

      self.user = user
      @password = password
      begin
        do_login(user, password)
      rescue InvalidUserCredentialsError => ex
        raise ex unless keychain_entry

        if keychain_entry.invalid_credentials
          login(user)
        else
          puts "Please run this tool again to apply the new password"
        end
      end
    end

    # This method is used for both the Apple Dev Portal and iTunes Connect
    # This will also handle 2 step verification
    def send_shared_login_request(user, password)
      # First we see if we have a stored cookie for 2 step enabled accounts
      # this is needed as it stores the information on if this computer is a
      # trusted one. In general I think spaceship clients should be trusted
      load_session_from_file
      # If this is a CI, the user can pass the session via environment variable
      load_session_from_env

      data = {
        accountName: user,
        password: password,
        rememberMe: true
      }

      begin
        # The below workaround is only needed for 2 step verified machines
        # Due to escaping of cookie values we have a little workaround here
        # By default the cookie jar would generate the following header
        #   DES5c148...=HSARM.......xaA/O69Ws/CHfQ==SRVT
        # However we need the following
        #   DES5c148...="HSARM.......xaA/O69Ws/CHfQ==SRVT"
        # There is no way to get the cookie jar value with " around the value
        # so we manually modify the cookie (only this one) to be properly escaped
        # Afterwards we pass this value manually as a header
        # It's not enough to just modify @cookie, it needs to be done after self.cookie
        # as a string operation
        important_cookie = @cookie.store.entries.find { |a| a.name.include?("DES") }
        if important_cookie
          modified_cookie = self.cookie # returns a string of all cookies
          unescaped_important_cookie = "#{important_cookie.name}=#{important_cookie.value}"
          escaped_important_cookie = "#{important_cookie.name}=\"#{important_cookie.value}\""
          modified_cookie.gsub!(unescaped_important_cookie, escaped_important_cookie)
        end

        response = request(:post) do |req|
          req.url "https://idmsa.apple.com/appleauth/auth/signin?widgetKey=#{itc_service_key}"
          req.body = data.to_json
          req.headers['Content-Type'] = 'application/json'
          req.headers['X-Requested-With'] = 'XMLHttpRequest'
          req.headers['Accept'] = 'application/json, text/javascript'
          req.headers["Cookie"] = modified_cookie if modified_cookie
        end
      rescue UnauthorizedAccessError
        raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
      end

      # get woinst, wois, and itctx cookie values
      request(:get, "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/wa/route?noext")

      case response.status
      when 403
        raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
      when 200
        return response
      else
        if response["Location"] == "/auth" # redirect to 2 step auth page
          handle_two_step(response)
          return true
        elsif (response.body || "").include?('invalid="true"')
          # User Credentials are wrong
          raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."<commit_msg>Fixed bug 2 step auth login (#6130)<commit_after>require 'faraday' # HTTP Client
require 'logger'
require 'faraday_middleware'
require 'faraday-cookie_jar'
require 'spaceship/ui'
require 'spaceship/helper/plist_middleware'
require 'spaceship/helper/net_http_generic_request'

Faraday::Utils.default_params_encoder = Faraday::FlatParamsEncoder

if ENV["DEBUG"]
  require 'openssl'
  # this has to be on top of this file, since the value can't be changed later
  OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
end

module Spaceship
  class Client
    PROTOCOL_VERSION = "QH65B2"
    USER_AGENT = "Spaceship #{Spaceship::VERSION}"

    attr_reader :client

    # The user that is currently logged in
    attr_accessor :user

    # The logger in which all requests are logged
    # /tmp/spaceship[time]_[pid].log by default
    attr_accessor :logger

    attr_accessor :csrf_tokens

    # Base class for errors that want to present their message as
    # preferred error info for fastlane error handling. See:
    # fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb
    class BasicPreferredInfoError < StandardError
      TITLE = 'The request could not be completed because:'.freeze

      def preferred_error_info
        message ? [TITLE, message] : nil
      end
    end

    # Invalid user credentials were provided
    class InvalidUserCredentialsError < BasicPreferredInfoError; end

    # Raised when no user credentials were passed at all
    class NoUserCredentialsError < BasicPreferredInfoError; end

    class UnexpectedResponse < StandardError
      attr_reader :error_info

      def initialize(error_info = nil)
        super(error_info)
        @error_info = error_info
      end

      def preferred_error_info
        return nil unless @error_info.kind_of?(Hash) && @error_info['resultString']

        [
          "Apple provided the following error info:",
          @error_info['resultString'],
          @error_info['userString']
        ].compact.uniq # sometimes 'resultString' and 'userString' are the same value
      end
    end

    # Raised when 302 is received from portal request
    class AppleTimeoutError < BasicPreferredInfoError; end

    # Raised when 401 is received from portal request
    class UnauthorizedAccessError < BasicPreferredInfoError; 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
    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

    def self.hostname
      raise "You must implemented self.hostname"
    end

    def initialize
      options = {
       request: {
          timeout:       300,
          open_timeout:  300
        }
      }
      @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.adapter Faraday.default_adapter

        if ENV['DEBUG']
          # for debugging only
          # This enables tracking of networking requests using Charles Web Proxy
          c.response :logger
          c.proxy "https://127.0.0.1:8888"
        end
      end
    end

    # The logger in which all requests are logged
    # /tmp/spaceship[time]_[pid].log by default
    def logger
      unless @logger
        if ENV["VERBOSE"]
          @logger = Logger.new(STDOUT)
        else
          # Log to file by default
          path = "/tmp/spaceship#{Time.now.to_i}_#{Process.pid}.log"
          @logger = Logger.new(path)
        end

        @logger.formatter = proc do |severity, datetime, progname, msg|
          "[#{datetime.strftime('%H:%M:%S')}]: #{msg}\n"
        end
      end

      @logger
    end

    ##
    # Return the session cookie.
    #
    # @return (String) the cookie-string in the RFC6265 format: https://tools.ietf.org/html/rfc6265#section-4.2.1
    def cookie
      @cookie.map(&:to_s).join(';')
    end

    def store_cookie(path: nil)
      path ||= persistent_cookie_path

      # really important to specify the session to true
      # otherwise myacinfo and more won't be stored
      @cookie.save(path, :yaml, session: true)
      return File.read(path)
    end

    def persistent_cookie_path
      path = File.expand_path(File.join("~", ".spaceship", self.user, "cookie"))
      FileUtils.mkdir_p(File.expand_path("..", path))
      return path
    end

    #####################################################
    # @!group Automatic Paging
    #####################################################

    # The page size we want to request, defaults to 500
    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
    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

    #####################################################
    # @!group Login and Team Selection
    #####################################################

    # 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
    def login(user = nil, password = nil)
      if user.to_s.empty? or password.to_s.empty?
        require 'credentials_manager'

        keychain_entry = CredentialsManager::AccountManager.new(user: user, password: password)
        user ||= keychain_entry.user
        password = keychain_entry.password
      end

      if user.to_s.strip.empty? or password.to_s.strip.empty?
        raise NoUserCredentialsError.new, "No login data provided"
      end

      self.user = user
      @password = password
      begin
        do_login(user, password)
      rescue InvalidUserCredentialsError => ex
        raise ex unless keychain_entry

        if keychain_entry.invalid_credentials
          login(user)
        else
          puts "Please run this tool again to apply the new password"
        end
      end
    end

    # This method is used for both the Apple Dev Portal and iTunes Connect
    # This will also handle 2 step verification
    def send_shared_login_request(user, password)
      # First we see if we have a stored cookie for 2 step enabled accounts
      # this is needed as it stores the information on if this computer is a
      # trusted one. In general I think spaceship clients should be trusted
      load_session_from_file
      # If this is a CI, the user can pass the session via environment variable
      load_session_from_env

      data = {
        accountName: user,
        password: password,
        rememberMe: true
      }

      begin
        # The below workaround is only needed for 2 step verified machines
        # Due to escaping of cookie values we have a little workaround here
        # By default the cookie jar would generate the following header
        #   DES5c148...=HSARM.......xaA/O69Ws/CHfQ==SRVT
        # However we need the following
        #   DES5c148...="HSARM.......xaA/O69Ws/CHfQ==SRVT"
        # There is no way to get the cookie jar value with " around the value
        # so we manually modify the cookie (only this one) to be properly escaped
        # Afterwards we pass this value manually as a header
        # It's not enough to just modify @cookie, it needs to be done after self.cookie
        # as a string operation
        important_cookie = @cookie.store.entries.find { |a| a.name.include?("DES") }
        if important_cookie
          modified_cookie = self.cookie # returns a string of all cookies
          unescaped_important_cookie = "#{important_cookie.name}=#{important_cookie.value}"
          escaped_important_cookie = "#{important_cookie.name}=\"#{important_cookie.value}\""
          modified_cookie.gsub!(unescaped_important_cookie, escaped_important_cookie)
        end

        response = request(:post) do |req|
          req.url "https://idmsa.apple.com/appleauth/auth/signin?widgetKey=#{itc_service_key}"
          req.body = data.to_json
          req.headers['Content-Type'] = 'application/json'
          req.headers['X-Requested-With'] = 'XMLHttpRequest'
          req.headers['Accept'] = 'application/json, text/javascript'
          req.headers["Cookie"] = modified_cookie if modified_cookie
        end
      rescue UnauthorizedAccessError
        raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
      end

      # get woinst, wois, and itctx cookie values
      request(:get, "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/wa/route?noext")

      case response.status
      when 403
        raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
      when 200
        return response
      else
        location = response["Location"]
        if location && URI.parse(location).path == "/auth" # redirect to 2 step auth page
          handle_two_step(response)
          return true
        elsif (response.body || "").include?('invalid="true"')
          # User Credentials are wrong
          raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
{'fraction_non_alphanumeric': 0.09973579920739763, 'fraction_numerical': 0.006463483676165315, 'mean_word_length': 2.6698407202216066, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 5, 'https://': 8, 'lorem ipsum': 0, 'www.': 0, 'xml': 4}, 'pii_count': 2, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '344319', 'n_tokens_mistral': 5931, 'n_tokens_neox': 5653, 'n_words': 1938}