lib/selenium/webdriver/remote/http/default.rb



# frozen_string_literal: true

# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
require 'ipaddr'

module Selenium
  module WebDriver
    module Remote
      module Http
        # @api private
        class Default < Common
          # Initializes object.
          # Warning: Setting {#open_timeout} to non-nil values will cause a separate thread to spawn.
          # Debuggers that freeze the process will not be able to evaluate any operations if that happens.
          # @param [ClientConfig] client_config - Configuration used to build the HTTP client.
          # @param [Numeric] open_timeout - Open timeout to apply to HTTP client.
          # @param [Numeric] read_timeout - Read timeout (seconds) to apply to HTTP client.
          def initialize(client_config: nil, open_timeout: nil, read_timeout: nil)
            if client_config && (open_timeout || read_timeout)
              raise ArgumentError, 'Cannot use both :client_config and :open_timeout/:read_timeout'
            end

            client_config ||= ClientConfig.new
            client_config.open_timeout = open_timeout if open_timeout
            client_config.read_timeout = read_timeout if read_timeout
            super(client_config: client_config)
          end

          def open_timeout
            client_config.open_timeout
          end

          def read_timeout
            client_config.read_timeout
          end

          def open_timeout=(value)
            client_config.open_timeout = value
          end

          def read_timeout=(value)
            client_config.read_timeout = value
          end

          def proxy=(value)
            client_config.proxy = value
          end

          def close
            @http&.finish
          end

          private

          def http
            @http ||= begin
              http = new_http_client
              if server_url.scheme == 'https'
                http.use_ssl = true
                http.verify_mode = OpenSSL::SSL::VERIFY_NONE
              end

              http.open_timeout = open_timeout if open_timeout
              http.read_timeout = read_timeout if read_timeout

              start(http)
              http
            end
          end

          def start(http)
            http.start
          end

          MAX_RETRIES = 3

          def request(verb, url, headers, payload, redirects = 0)
            retries = 0

            begin
              request = new_request_for(verb, url, headers, payload)
              response = response_for(request)
            rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EADDRINUSE, Errno::EADDRNOTAVAIL
              # a retry is sometimes needed:
              #   on Windows XP where we may quickly run out of ephemeral ports
              #   when the port becomes temporarily unavailable
              #
              # A more robust solution is bumping the MaxUserPort setting
              # as described here:
              #
              # http://msdn.microsoft.com/en-us/library/aa560610%28v=bts.20%29.aspx
              raise if retries >= MAX_RETRIES

              retries += 1
              sleep 2
              retry
            rescue Errno::ECONNREFUSED => e
              raise e.class, "using proxy: #{proxy.http}" if use_proxy?

              raise
            end

            if response.is_a? Net::HTTPRedirection
              follow_redirect(response, redirects)
            else
              WebDriver.logger.debug("   <<<  #{response.instance_variable_get(:@header).inspect}", id: :header)
              create_response response.code, response.body, response.content_type
            end
          end

          def follow_redirect(response, redirects)
            WebDriver.logger.debug("Redirect to #{response['Location']}; times: #{redirects}")
            raise Error::WebDriverError, 'too many redirects' if redirects >= client_config.max_redirects

            request(:get, URI.parse(response['Location']), DEFAULT_HEADERS.dup, nil, redirects + 1)
          end

          def new_request_for(verb, url, headers, payload)
            req = Net::HTTP.const_get(verb.to_s.capitalize).new(url.path, headers)

            req.basic_auth server_url.user, server_url.password if server_url.userinfo

            req.body = payload if payload

            req
          end

          def response_for(request)
            http.request request
          end

          def new_http_client
            if use_proxy?
              url = proxy.http
              unless proxy.respond_to?(:http) && url
                raise Error::WebDriverError,
                      "expected HTTP proxy, got #{proxy.inspect}"
              end

              proxy_uri = URI.parse(url)

              Net::HTTP.new(server_url.host, server_url.port,
                            proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
            else
              Net::HTTP.new server_url.host, server_url.port
            end
          end

          def proxy
            client_config.proxy
          end

          def use_proxy?
            return false if proxy.nil?
            return true unless proxy.no_proxy

            !proxy_ignored?
          end

          def proxy_ignored?
            proxy.no_proxy.split(',').map(&:strip).reject(&:empty?).any? do |host|
              host == '*' || host == server_url.host || ip_match?(host)
            end
          end

          def ip_match?(host)
            IPAddr.new(host).include?(server_url.host)
          rescue ArgumentError
            false
          end
        end # Default
      end # Http
    end # Remote
  end # WebDriver
end # Selenium