# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'socket' cs__scoped_require 'uri' cs__scoped_require 'contrast/api/speedracer' cs__scoped_require 'contrast/api/tcp_socket' cs__scoped_require 'contrast/api/unix_socket' cs__scoped_require 'contrast/api/connection_status' cs__scoped_require 'contrast/components/interface' module Contrast module Agent # SocketClient acts as a interface between the agent and the service. It instantiates a # service proxy and tracks the state of that proxy. class SocketClient include Contrast::Components::Interface access_component :app_context, :contrast_service, :logging attr_reader :speedracer, :status def initialize @status = Contrast::Api::ConnectionStatus.new @speedracer = Contrast::Api::Speedracer.new(init_connection) @ensure_running = Mutex.new end def init_connection if CONTRAST_SERVICE.use_tcp? Contrast::Api::TcpSocket.new(CONTRAST_SERVICE.host, CONTRAST_SERVICE.port) else Contrast::Api::UnixSocket.new(CONTRAST_SERVICE.socket_path) end end def service_unavailable? speedracer.nil? end def connection_established? status.connected? end # Attempt to send the passed message to SpeedRacer. If we're using bundled # service config option, we must first start up the service. Afterwards, we # must send agent startup messages no matter what the config options are. # # @param event [Contrast::Api::Dtm] One of the DTMs valid for the event # field of Contrast::Api::Dtm::Message # @return [Array] the response from SpeedRacer or nil # if it failed to send or the service is unavailable. def send_to_speedracer event return if service_unavailable? @ensure_running.synchronize do if CONTRAST_SERVICE.use_bundled_service? return unless start_or_restart end build_service_context unless status.startup_messages_sent? end logger.debug_with_time(event.cs__class.name) do response = speedracer.send_one event status.success! response end rescue StandardError => e status.failure! logger.error('Unable to send message.', e, event_id: event.__id__, event_type: event.cs__class.name) nil end private def restart? status.was_connected? && status.failed? end # Now that we know we're running with bundled service option, we can tell SpeedRacer that we're # ready for it to be started up. # # @return [Boolean] true if the service is started, false if the service is still offline after # we've attempted to start it up. def start_or_restart if restart? logger.info('Attempting to restart service.') return speedracer.start_service elsif !status.was_connected? logger.info('Attempting to start service.') return speedracer.start_service end true end def build_service_context agent_startup_msg = APP_CONTEXT.build_agent_startup_message app_startup_msg = APP_CONTEXT.build_app_startup_message # 1 initial attempt, + 3 potential retries. # The agent-service-api REQUIREMENTS.md spec mandates this #. 4.times do log_send_event(agent_startup_msg) next unless (agent_response = speedracer.send_one(agent_startup_msg)) # Connection was successful log_send_event(app_startup_msg) app_response = speedracer.send_one(app_startup_msg) [agent_response, app_response].each do |msg| Contrast::Utils::ServiceResponseUtil.process_response msg end status.success! logger.debug('Successfully sent startup messages to service.') return end logger.error('Could not send agent startup message context') rescue StandardError => e logger.error('Could not build service context', e) raise # re-raise the error to be caught by SocketClient#send_to_speedracer end def log_send_event event logger.debug('Immediately sending event.', event_id: event.__id__, event_type: event.cs__class.name) end end end end