# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'socket' require 'uri' require 'contrast/api/communication/tcp_socket' require 'contrast/api/communication/unix_socket' require 'contrast/components/logger' module Contrast module Api module Communication # 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::Logger::InstanceMethods def initialize @socket = init_connection end # Wrap the given DTM in a Contrast::Api::Dtm::Message and send it to the SpeedRacer for processing. The # Message is the top level object required to communicate to SpeedRacer as it encompasses the information # needed to find this process' context. # # @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of # Contrast::Api::Dtm::Message # @return [Contrast::Api::Settings::AgentSettings, nil] def send_one event msg = Contrast::Api::Dtm::Message.build(event) send_message(msg) end private # Initialize the connection to the SpeedRacer process based on the configuration provided by the user. This can # be either TCP or UDP. Note that unlike the Go and the Node Agents, we cannot use the GRPC communication # option as we cannot use Google's protobuf gems; they do not compile reliably and result in segmentation # faults in customer environments. # # @return [Contrast::Api::Communication::TcpSocket, Contrast::Api::Communication::UnixSocket] def init_connection log_connection if ::Contrast::CONTRAST_SERVICE.use_tcp? Contrast::Api::Communication::TcpSocket.new( ::Contrast::CONTRAST_SERVICE.host, ::Contrast::CONTRAST_SERVICE.port) else Contrast::Api::Communication::UnixSocket.new(::Contrast::CONTRAST_SERVICE.socket_path) end end # Log information about the connection being used to communicate between the Agent and SpeedRacer. def log_connection # The socket is set, if ::Contrast::CONFIG.root.agent.service.socket logger.info('Connecting to the Contrast Service using a UnixSocket socket', socket: ::Contrast::CONTRAST_SERVICE.socket_path) return end # The host & port are set, if ::Contrast::CONFIG.root.agent.service.host && ::Contrast::CONFIG.root.agent.service.port logger.info('Connecting to the Contrast Service using a TCP socket', host: ::Contrast::CONTRAST_SERVICE.host, port: ::Contrast::CONTRAST_SERVICE.port) return end # Or something is not set. logger.warn( log_connection_error_msg, host: ::Contrast::CONTRAST_SERVICE.host, port: ::Contrast::CONTRAST_SERVICE.port) end # If our connection isn't built properly, we need to warn the user. This builds out the context specific # message to provide that warning # # @return [String] def log_connection_error_msg if ::Contrast::CONFIG.root.agent.service.host 'Missing a required connection value to the Contrast Service. ' \ '`agent.service.port` is not set. ' \ 'Falling back to default TCP socket port.' elsif ::Contrast::CONFIG.root.agent.service.port 'Missing a required connection value to the Contrast Service. ' \ '`agent.service.host` is not set. ' \ 'Falling back to default TCP socket host.' else 'Missing a required connection value to the Contrast Service. ' \ 'Neither `agent.service.socket` nor the pair of `agent.service.host` and `agent.service.port` are set. '\ 'Falling back to default TCP socket.' end end # Send the given message to SpeedRacer and return the response from it. # # @param msg [Contrast::Api::Dtm::Message] the packaged message to send to SpeedRacer # @return [Contrast::Api::Settings::AgentSettings, nil] # @raise [StandardError] if unable to send a message to SpeedRacer 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('RESPONSE FROM TS') logger.debug(from_service) logger.debug('RESPONSE FROM TS') 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 SpeedRacer manage the connection end # Send the marshaled Contrast::Api::Dtm::Message across the socket used to talk to SpeedRacer # # @param marshaled [String] # @return [String] the marshaled from of Contrast::Api::Settings::AgentSettings def send_marshaled marshaled @socket.send_marshaled(marshaled) end end end end end