# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'contrast/utils/object_share' cs__scoped_require 'contrast/utils/os' cs__scoped_require 'contrast/components/interface' module Contrast module Api # This class acts as the interface for the communication between the Agent # and the Service (SpeedRacer) as well as functions as a way for the Agent # to manage the bundled Service. class Speedracer include Contrast::Components::Interface access_component :app_context, :contrast_service, :logging attr_reader :client_number, :host, :port, :socket, :connection @instance_count = 0 @instance_mutex = Mutex.new def self.next_client_number @instance_mutex.synchronize do @instance_count += 1 # Rollover things rescue StandardError @instance_count = 1 end end def self.read_only_instance_count @instance_count.dup end def initialize connection @client_number = Contrast::Api::Speedracer.next_client_number @messages_total = 0 @pid = Process.pid.to_i @ppid = Process.ppid.to_i @connection = connection end # Allow for the start of a Service, but block so that we only attempt to # start the service one try at a time in the case multiple threads are # trying to send messages. def start_service zombie_check service_starter_thread.join(5) is_service_started = Contrast::Utils::OS.running? logger.error('The bundled service could not be started. The agent will not function properly.') unless is_service_started is_service_started end # Wrap the given DTM in a Contrast::Api::Dtm::Message and send it to the # Service for processing # # @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of # Contrast::Api::Dtm::Message def send_one event msg = build_message(event) send_message(msg) end private # check if there's a zombie service that exists, and wait on it if so. # currently, this only happens when trying to initialize speedracer def zombie_check zombie_pid_list = Contrast::Utils::OS.zombie_pids zombie_pid_list.each do |pid| Process.wait(pid.to_i) end end def determine_startup_options return { out: :out, err: :err } if Contrast::Agent::FeatureState.instance.service_control[:logger_path] == 'STDOUT' { out: File::NULL, err: File::NULL } end # This is a separate method so we can overwrite it globally in specs def spawn_service options = determine_startup_options spawn 'contrast_service', options end def service_starter_thread Contrast::Agent::Thread.new do # Always check to see if it already started unless Contrast::Utils::OS.running? # Spawn the service process spawn_service # Block until service is running sleep(0.1) until Contrast::Utils::OS.running? end end end # Wrap the given event in a Contrast::Api::Dtm::Message # # @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of # Contrast::Api::Dtm::Message # @return Contrast::Api::Dtm::Message def build_message event message = base_message case event when Contrast::Api::Dtm::ServerActivity message.server_activity = event when Contrast::Api::Dtm::AgentStartup message.agent_startup = event when Contrast::Api::Dtm::ApplicationCreate message.application_create = event when Contrast::Api::Dtm::ApplicationUpdate message.application_update = event when Contrast::Api::Dtm::Activity message.activity = event when Contrast::Api::Dtm::HttpRequest message.prefilter = event when Contrast::Api::Dtm::HttpResponse message.postfilter = event when Contrast::Api::Dtm::Poll message.poll = event when Contrast::Api::Dtm::ObservedRoute message.observed_route = event else logger.error('Unknown event type received. Unsure how to send.', event_type: event.cs__class.name) return end logger.debug('Wrapping event in message', msg_id: message.__id__, p_id: message.pid, msg_count: message.message_count, event_id: event.__id__, event_type: event.cs__class.name) message end def base_message msg = Contrast::Api::Dtm::Message.new msg.app_name = APP_CONTEXT.name msg.app_path = APP_CONTEXT.path msg.app_language = Contrast::Utils::ObjectShare::RUBY msg.client_id = APP_CONTEXT.client_id msg.client_number = client_number msg.client_total = Contrast::Api::Speedracer.read_only_instance_count msg.message_count = (@messages_total += 1) msg.pid = APP_CONTEXT.pid msg.ppid = APP_CONTEXT.ppid msg end def send_message msg return unless msg logger.debug('Sending message.', msg_id: msg.__id__, p_id: msg.pid, msg_count: msg.message_count) to_service = Contrast::Api::Dtm::Message.encode(msg) from_service = send_marshaled(to_service) response = Contrast::Api::Settings::AgentSettings.decode(from_service) logger.debug('Received response.', msg_id: msg.__id__, p_id: msg.pid, msg_count: msg.message_count, response_id: response&.__id__) response rescue StandardError => e logger.error('Sending failed for message.', e, msg_id: msg.__id__, p_id: msg.pid, msg_count: msg.message_count, response_id: response&.__id__) raise e # reraise to let SocketClient manage the connection end def send_marshaled marshaled connection.send_marshaled(marshaled) end end end end