# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'net/http' require 'contrast/components/logger' require 'contrast/utils/object_share' require 'socket' module Contrast module Utils # This module creates a Net::HTTP client base to be used by different services # All HTTP clients reporting to Telemetry or TS should inherit from this class NetHttpBase include Contrast::Components::Logger::InstanceMethods # This method initializes the Net::HTTP client we'll need. it will validate # the connection and make the first request. If connection is valid and response # is available then the open connection is returned. # # @param service_name [String] Name of service used in logging messages # @param url [String] # @param use_proxy [Boolean] flag used to indicate proxy connections [default = false] # @param use_custom_cert [Boolean] flag used to indicate whether the client is to use # self signed certificates provided by config [default = false] # @return [Net::HTTP, nil] Return open connection or nil def initialize_connection service_name, url, use_proxy: false, use_custom_cert: false return unless url addr = URI(url) # the proxy is enabled only if there is provided url even if the enable is set to true return if addr.host.nil? || addr.port.nil? return if addr.scheme != 'https' && !addr.host.to_s.include?('localhost') # TODO: RUBY-99999 allow http w/ localhost # rubocop:disable Layout/LineLength proxy_addr = URI(Contrast::API.proxy_url) if proxy_enabled? net_http_client = initialize_client addr, proxy_addr, use_proxy, use_custom_cert return if net_http_client.nil? net_http_client.start return unless net_http_client.started? logger.debug("Starting #{ service_name } connection test") return unless connection_verified? net_http_client logger.debug('Client verified', service: service_name, url: url) net_http_client rescue StandardError => e logger.error('Connection failed', e, service: service_name, url: url) nil end # Validates connection with assigned domain. # If connection is running, SSL certificate of the endpoint is valid, Ip address is resolvable # and response is received without peer's reset or refuse of connection, # then validation returns true. Error handling is in place so that the work of the agent will continue as # normal without Telemetry. # # @param client [Net::HTTP] # @return [Boolean] true | false def connection_verified? client return @_connection_verified unless @_connection_verified.nil? return false if client.nil? ipaddr = get_ipaddr(client) response = client.request(Net::HTTP::Get.new(client.address)) verify_cert = client.address.to_s.include?('localhost') || OpenSSL::SSL.verify_certificate_identity(client.peer_cert, client.address) resolved = resolved? client.address, ipaddr @_connection_verified = if resolved && response && verify_cert true else false end rescue OpenSSL::SSL::SSLError, Resolv::ResolvError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::ESHUTDOWN, Errno::EHOSTDOWN, Errno::EHOSTUNREACH, Errno::EISCONN, Errno::ECONNABORTED, Errno::ENETRESET, Errno::ENETUNREACH => e logger.warn("#{ service_name } connection failed", e.message) false end private # Resolves the address of the assigned domain to array of corresponding IPs (if more than one) # and runs a matcher to see if current connection IP is in the list. # This is called within #verify_connection, if called on it's own there will be no # error handling. # # @param address [String] Human friendly address of assigned domain # @param ipaddr [String] Machine friendly IP address of the assigned domain # @return[Boolean] true if both addresses are resolved | false if one of the addresses # is non-resolvable def resolved? address, ipaddr return @_resolved unless @_resolved.nil? @_resolved = if (addresses = Resolv.getaddresses address) addresses.any? { |addr| addr.include?(ipaddr) } else false end end # if the configuration for the use of self-signed certificates is enabled # and required for this client then assign the files and keys to the client # # @param client [Net::HTTP] def assign_cert client if Contrast::API.certification_ca_file client.ca_file = OpenSSL::X509::Certificate.new(File.read(Contrast::API.certification_ca_file)).to_s elsif Contrast::API.certification_cert_file client.cert = OpenSSL::X509::Certificate.new(File.read(Contrast::API.certification_cert_file)).to_s elsif Contrast::API.certification_key_file client.key = OpenSSL::PKey::RSA.new(File.read(Contrast::API.certification_key_file)).to_s end rescue Errno::ENOENT => e logger.error('Custom certificates failed', e.message) end # sets default setting for client validation of certificates and # timeout options # # @param addr [URI::Generic] uri of assigned domain # @param proxy_addr [URI::Generic | nil] uri of proxy # @param use_proxy [Boolean] flag used to indicate proxy connections [default = false] # @param use_custom_cert [Boolean] flag used to indicate whether the client is to use # self signed certificates provided by config [default = false] # @return [Net::HTTP] def initialize_client addr, proxy_addr, use_proxy, use_custom_cert initialize_client = if proxy_enabled? && use_proxy Net::HTTP.new(addr.host, nil, proxy_addr&.host, proxy_addr&.port) else Net::HTTP.new(addr.host, addr.port) end return initialize_client if addr.host.to_s.include?('localhost') # TODO: RUBY-99999 allow http w/ localhost assign_cert(initialize_client) if use_custom_cert && Contrast::API.certification_enabled? initialize_client.use_ssl = true initialize_client.verify_mode = OpenSSL::SSL::VERIFY_PEER initialize_client.verify_depth = 5 # open connection timeout in ms # if connection reaches timeout this will produce Net::OpenTimeout error initialize_client.open_timeout = 10 # if we can't read the response or a chunk within time this will cause a # Net::ReadTimeout error when the request is made initialize_client.read_timeout = 10 initialize_client end # Check if proxy is enabled # # @return @_proxy_enabled [Boolean] True if proxy is enabled and url is present else false def proxy_enabled? return @_proxy_enabled unless @_proxy_enabled.nil? @_proxy_enabled = Contrast::API.proxy_enabled? && !Contrast::API.proxy_url.nil? end # Retrieve the IP address from the client. # # @param client [Net::HTTP] # @return [String] def get_ipaddr client socket = TCPSocket.open(client.address, client.port) ipaddr = socket.peeraddr[3] socket.close ipaddr end end end end